]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/buffer.jsm
Import 1.0rc1 supporting Firefox up to 11.*
[dactyl.git] / common / modules / buffer.jsm
1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
4 //
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
7 try {"use strict";
8
9 Components.utils.import("resource://dactyl/bootstrap.jsm");
10 defineModule("buffer", {
11     exports: ["Buffer", "buffer"],
12     require: ["prefs", "services", "util"]
13 }, this);
14
15 this.lazyRequire("finder", ["RangeFind"]);
16 this.lazyRequire("overlay", ["overlay"]);
17 this.lazyRequire("storage", ["storage"]);
18 this.lazyRequire("template", ["template"]);
19
20 /**
21  * A class to manage the primary web content buffer. The name comes
22  * from Vim's term, 'buffer', which signifies instances of open
23  * files.
24  * @instance buffer
25  */
26 var Buffer = Module("Buffer", {
27     Local: function Local(dactyl, modules, window) ({
28         get win() {
29             return window.content;
30
31             let win = services.focus.focusedWindow;
32             if (!win || win == window || util.topWindow(win) != window)
33                 return window.content
34             if (win.top == window)
35                 return win;
36             return win.top;
37         }
38     }),
39
40     init: function init(win) {
41         if (win)
42             this.win = win;
43     },
44
45     get addPageInfoSection() Buffer.closure.addPageInfoSection,
46
47     get pageInfo() Buffer.pageInfo,
48
49     // called when the active document is scrolled
50     _updateBufferPosition: function _updateBufferPosition() {
51         this.modules.statusline.updateBufferPosition();
52         this.modules.commandline.clear(true);
53     },
54
55     /**
56      * @property {Array} The alternative style sheets for the current
57      *     buffer. Only returns style sheets for the 'screen' media type.
58      */
59     get alternateStyleSheets() {
60         let stylesheets = array.flatten(
61             this.allFrames().map(function (w) Array.slice(w.document.styleSheets)));
62
63         return stylesheets.filter(
64             function (stylesheet) /^(screen|all|)$/i.test(stylesheet.media.mediaText) && !/^\s*$/.test(stylesheet.title)
65         );
66     },
67
68     climbUrlPath: function climbUrlPath(count) {
69         let { dactyl } = this.modules;
70
71         let url = this.documentURI.clone();
72         dactyl.assert(url instanceof Ci.nsIURL);
73
74         while (count-- && url.path != "/")
75             url.path = url.path.replace(/[^\/]+\/*$/, "");
76
77         dactyl.assert(!url.equals(this.documentURI));
78         dactyl.open(url.spec);
79     },
80
81     incrementURL: function incrementURL(count) {
82         let { dactyl } = this.modules;
83
84         let matches = this.uri.spec.match(/(.*?)(\d+)(\D*)$/);
85         dactyl.assert(matches);
86         let oldNum = matches[2];
87
88         // disallow negative numbers as trailing numbers are often proceeded by hyphens
89         let newNum = String(Math.max(parseInt(oldNum, 10) + count, 0));
90         if (/^0/.test(oldNum))
91             while (newNum.length < oldNum.length)
92                 newNum = "0" + newNum;
93
94         matches[2] = newNum;
95         dactyl.open(matches.slice(1).join(""));
96     },
97
98     /**
99      * @property {number} True when the buffer is fully loaded.
100      */
101     get loaded() Math.min.apply(null,
102         this.allFrames()
103             .map(function (frame) ["loading", "interactive", "complete"]
104                                       .indexOf(frame.document.readyState))),
105
106     /**
107      * @property {Object} The local state store for the currently selected
108      *     tab.
109      */
110     get localStore() {
111         let { doc } = this;
112
113         let store = overlay.getData(doc, "buffer", null);
114         if (!store || !this.localStorePrototype.isPrototypeOf(store))
115             store = overlay.setData(doc, "buffer", Object.create(this.localStorePrototype));
116         return store.instance = store;
117     },
118
119     localStorePrototype: memoize({
120         instance: {},
121         get jumps() [],
122         jumpsIndex: -1
123     }),
124
125     /**
126      * @property {Node} The last focused input field in the buffer. Used
127      *     by the "gi" key binding.
128      */
129     get lastInputField() {
130         let field = this.localStore.lastInputField && this.localStore.lastInputField.get();
131
132         let doc = field && field.ownerDocument;
133         let win = doc && doc.defaultView;
134         return win && doc === win.document ? field : null;
135     },
136     set lastInputField(value) { this.localStore.lastInputField = util.weakReference(value); },
137
138     /**
139      * @property {nsIURI} The current top-level document.
140      */
141     get doc() this.win.document,
142
143     get docShell() util.docShell(this.win),
144
145     get modules() this.topWindow.dactyl.modules,
146     set modules(val) {},
147
148     topWindow: Class.Memoize(function () util.topWindow(this.win)),
149
150     /**
151      * @property {nsIURI} The current top-level document's URI.
152      */
153     get uri() util.newURI(this.win.location.href),
154
155     /**
156      * @property {nsIURI} The current top-level document's URI, sans any
157      *     fragment identifier.
158      */
159     get documentURI() this.doc.documentURIObject || util.newURI(this.doc.documentURI),
160
161     /**
162      * @property {string} The current top-level document's URL.
163      */
164     get URL() update(new String(this.win.location.href), util.newURI(this.win.location.href)),
165
166     /**
167      * @property {number} The buffer's height in pixels.
168      */
169     get pageHeight() this.win.innerHeight,
170
171     get contentViewer() this.docShell.contentViewer
172                                      .QueryInterface(Components.interfaces.nsIMarkupDocumentViewer),
173
174     /**
175      * @property {number} The current browser's zoom level, as a
176      *     percentage with 100 as 'normal'.
177      */
178     get zoomLevel() {
179         let v = this.contentViewer;
180         return v[v.textZoom == 1 ? "fullZoom" : "textZoom"] * 100
181     },
182     set zoomLevel(value) { this.setZoom(value, this.fullZoom); },
183
184     /**
185      * @property {boolean} Whether the current browser is using full
186      *     zoom, as opposed to text zoom.
187      */
188     get fullZoom() this.ZoomManager.useFullZoom,
189     set fullZoom(value) { this.setZoom(this.zoomLevel, value); },
190
191     get ZoomManager() this.topWindow.ZoomManager,
192
193     /**
194      * @property {string} The current document's title.
195      */
196     get title() this.doc.title,
197
198     /**
199      * @property {number} The buffer's horizontal scroll percentile.
200      */
201     get scrollXPercent() {
202         let elem = Buffer.Scrollable(this.findScrollable(0, true));
203         if (elem.scrollWidth - elem.clientWidth === 0)
204             return 0;
205         return elem.scrollLeft * 100 / (elem.scrollWidth - elem.clientWidth);
206     },
207
208     /**
209      * @property {number} The buffer's vertical scroll percentile.
210      */
211     get scrollYPercent() {
212         let elem = Buffer.Scrollable(this.findScrollable(0, false));
213         if (elem.scrollHeight - elem.clientHeight === 0)
214             return 0;
215         return elem.scrollTop * 100 / (elem.scrollHeight - elem.clientHeight);
216     },
217
218     /**
219      * @property {{ x: number, y: number }} The buffer's current scroll position
220      * as reported by {@link Buffer.getScrollPosition}.
221      */
222     get scrollPosition() Buffer.getScrollPosition(this.findScrollable(0, false)),
223
224     /**
225      * Returns a list of all frames in the given window or current buffer.
226      */
227     allFrames: function allFrames(win, focusedFirst) {
228         let frames = [];
229         (function rec(frame) {
230             if (true || frame.document.body instanceof Ci.nsIDOMHTMLBodyElement)
231                 frames.push(frame);
232             Array.forEach(frame.frames, rec);
233         })(win || this.win);
234
235         if (focusedFirst)
236             return frames.filter(function (f) f === this.focusedFrame).concat(
237                    frames.filter(function (f) f !== this.focusedFrame));
238         return frames;
239     },
240
241     /**
242      * @property {Window} Returns the currently focused frame.
243      */
244     get focusedFrame() {
245         let frame = this.localStore.focusedFrame;
246         return frame && frame.get() || this.win;
247     },
248     set focusedFrame(frame) {
249         this.localStore.focusedFrame = util.weakReference(frame);
250     },
251
252     /**
253      * Returns the currently selected word. If the selection is
254      * null, it tries to guess the word that the caret is
255      * positioned in.
256      *
257      * @returns {string}
258      */
259     get currentWord() Buffer.currentWord(this.focusedFrame),
260     getCurrentWord: deprecated("buffer.currentWord", function getCurrentWord() Buffer.currentWord(this.focusedFrame, true)),
261
262     /**
263      * Returns true if a scripts are allowed to focus the given input
264      * element or input elements in the given window.
265      *
266      * @param {Node|Window}
267      * @returns {boolean}
268      */
269     focusAllowed: function focusAllowed(elem) {
270         if (elem instanceof Ci.nsIDOMWindow && !DOM(elem).isEditable)
271             return true;
272
273         let { options } = this.modules;
274
275         let doc = elem.ownerDocument || elem.document || elem;
276         switch (options.get("strictfocus").getKey(doc.documentURIObject || util.newURI(doc.documentURI), "moderate")) {
277         case "despotic":
278             return overlay.getData(elem)["focus-allowed"]
279                     || elem.frameElement && overlay.getData(elem.frameElement)["focus-allowed"];
280         case "moderate":
281             return overlay.getData(doc, "focus-allowed")
282                     || elem.frameElement && overlay.getData(elem.frameElement.ownerDocument)["focus-allowed"];
283         default:
284             return true;
285         }
286     },
287
288     /**
289      * Focuses the given element. In contrast to a simple
290      * elem.focus() call, this function works for iframes and
291      * image maps.
292      *
293      * @param {Node} elem The element to focus.
294      */
295     focusElement: function focusElement(elem) {
296         let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem;
297         overlay.setData(elem, "focus-allowed", true);
298         overlay.setData(win.document, "focus-allowed", true);
299
300         if (isinstance(elem, [Ci.nsIDOMHTMLFrameElement,
301                               Ci.nsIDOMHTMLIFrameElement]))
302             elem = elem.contentWindow;
303
304         if (elem.document)
305             overlay.setData(elem.document, "focus-allowed", true);
306
307         if (elem instanceof Ci.nsIDOMHTMLInputElement && elem.type == "file") {
308             Buffer.openUploadPrompt(elem);
309             this.lastInputField = elem;
310         }
311         else {
312             if (isinstance(elem, [Ci.nsIDOMHTMLInputElement,
313                                   Ci.nsIDOMXULTextBoxElement]))
314                 var flags = services.focus.FLAG_BYMOUSE;
315             else
316                 flags = services.focus.FLAG_SHOWRING;
317
318             // Hack to deal with current versions of Firefox misplacing
319             // the caret
320             if (!overlay.getData(elem, "had-focus", false) && elem.value &&
321                     elem instanceof Ci.nsIDOMHTMLInputElement &&
322                     DOM(elem).isEditable &&
323                     elem.selectionStart != null &&
324                     elem.selectionStart == elem.selectionEnd)
325                 elem.selectionStart = elem.selectionEnd = elem.value.length;
326
327             DOM(elem).focus(flags);
328
329             if (elem instanceof Ci.nsIDOMWindow) {
330                 let sel = elem.getSelection();
331                 if (sel && !sel.rangeCount)
332                     sel.addRange(RangeFind.endpoint(
333                         RangeFind.nodeRange(elem.document.body || elem.document.documentElement),
334                         true));
335             }
336             else {
337                 let range = RangeFind.nodeRange(elem);
338                 let sel = (elem.ownerDocument || elem).defaultView.getSelection();
339                 if (!sel.rangeCount || !RangeFind.intersects(range, sel.getRangeAt(0))) {
340                     range.collapse(true);
341                     sel.removeAllRanges();
342                     sel.addRange(range);
343                 }
344             }
345
346             // for imagemap
347             if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
348                 try {
349                     let [x, y] = elem.getAttribute("coords").split(",").map(parseFloat);
350
351                     DOM(elem).mouseover({ screenX: x, screenY: y });
352                 }
353                 catch (e) {}
354             }
355         }
356     },
357
358     /**
359      * Find the *count*th last link on a page matching one of the given
360      * regular expressions, or with a @rel or @rev attribute matching
361      * the given relation. Each frame is searched beginning with the
362      * last link and progressing to the first, once checking for
363      * matching @rel or @rev properties, and then once for each given
364      * regular expression. The first match is returned. All frames of
365      * the page are searched, beginning with the currently focused.
366      *
367      * If follow is true, the link is followed.
368      *
369      * @param {string} rel The relationship to look for.
370      * @param {[RegExp]} regexps The regular expressions to search for.
371      * @param {number} count The nth matching link to follow.
372      * @param {bool} follow Whether to follow the matching link.
373      * @param {string} path The CSS to use for the search. @optional
374      */
375     findLink: function findLink(rel, regexps, count, follow, path) {
376         let { Hints, dactyl, options } = this.modules;
377
378         let selector = path || options.get("hinttags").stringDefaultValue;
379
380         function followFrame(frame) {
381             function iter(elems) {
382                 for (let i = 0; i < elems.length; i++)
383                     if (elems[i].rel.toLowerCase() === rel || elems[i].rev.toLowerCase() === rel)
384                         yield elems[i];
385             }
386
387             let elems = frame.document.getElementsByTagName("link");
388             for (let elem in iter(elems))
389                 yield elem;
390
391             elems = frame.document.getElementsByTagName("a");
392             for (let elem in iter(elems))
393                 yield elem;
394
395             function a(regexp, elem) regexp.test(elem.textContent) === regexp.result ||
396                             Array.some(elem.childNodes, function (child) regexp.test(child.alt) === regexp.result);
397             function b(regexp, elem) regexp.test(elem.title);
398
399             let res = Array.filter(frame.document.querySelectorAll(selector), Hints.isVisible);
400             for (let test in values([a, b]))
401                 for (let regexp in values(regexps))
402                     for (let i in util.range(res.length, 0, -1))
403                         if (test(regexp, res[i]))
404                             yield res[i];
405         }
406
407         for (let frame in values(this.allFrames(null, true)))
408             for (let elem in followFrame(frame))
409                 if (count-- === 0) {
410                     if (follow)
411                         this.followLink(elem, dactyl.CURRENT_TAB);
412                     return elem;
413                 }
414
415         if (follow)
416             dactyl.beep();
417     },
418     followDocumentRelationship: deprecated("buffer.findLink",
419         function followDocumentRelationship(rel) {
420             let { options } = this.modules;
421
422             this.findLink(rel, options[rel + "pattern"], 0, true);
423         }),
424
425     /**
426      * Fakes a click on a link.
427      *
428      * @param {Node} elem The element to click.
429      * @param {number} where Where to open the link. See
430      *     {@link dactyl.open}.
431      */
432     followLink: function followLink(elem, where) {
433         let { dactyl } = this.modules;
434
435         let doc = elem.ownerDocument;
436         let win = doc.defaultView;
437         let { left: offsetX, top: offsetY } = elem.getBoundingClientRect();
438
439         if (isinstance(elem, [Ci.nsIDOMHTMLFrameElement,
440                               Ci.nsIDOMHTMLIFrameElement]))
441             return this.focusElement(elem);
442
443         if (isinstance(elem, Ci.nsIDOMHTMLLinkElement))
444             return dactyl.open(elem.href, where);
445
446         if (elem instanceof Ci.nsIDOMHTMLAreaElement) { // for imagemap
447             let coords = elem.getAttribute("coords").split(",");
448             offsetX = Number(coords[0]) + 1;
449             offsetY = Number(coords[1]) + 1;
450         }
451         else if (elem instanceof Ci.nsIDOMHTMLInputElement && elem.type == "file") {
452             Buffer.openUploadPrompt(elem);
453             return;
454         }
455
456         let { dactyl } = this.modules;
457
458         let ctrlKey = false, shiftKey = false;
459         switch (dactyl.forceTarget || where) {
460         case dactyl.NEW_TAB:
461         case dactyl.NEW_BACKGROUND_TAB:
462             ctrlKey = true;
463             shiftKey = dactyl.forceBackground != null ? dactyl.forceBackground
464                                                       : where != dactyl.NEW_BACKGROUND_TAB;
465             break;
466         case dactyl.NEW_WINDOW:
467             shiftKey = true;
468             break;
469         case dactyl.CURRENT_TAB:
470             break;
471         }
472
473         this.focusElement(elem);
474
475         prefs.withContext(function () {
476             prefs.set("browser.tabs.loadInBackground", true);
477             let params = {
478                 screenX: offsetX, screenY: offsetY,
479                 ctrlKey: ctrlKey, shiftKey: shiftKey, metaKey: ctrlKey
480             };
481
482             DOM(elem).mousedown(params).mouseup(params);
483             if (!config.haveGecko("2b"))
484                 DOM(elem).click(params);
485
486             let sel = util.selectionController(win);
487             sel.getSelection(sel.SELECTION_FOCUS_REGION).collapseToStart();
488         });
489     },
490
491     /**
492      * Resets the caret position so that it resides within the current
493      * viewport.
494      */
495     resetCaret: function resetCaret() {
496         function visible(range) util.intersection(DOM(range).rect, viewport);
497
498         function getRanges(rect) {
499             let nodes = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
500                            .nodesFromRect(rect.x, rect.y, 0, rect.width, rect.height, 0, false, false);
501             return Array.filter(nodes, function (n) n instanceof Ci.nsIDOMText)
502                         .map(RangeFind.nodeContents);
503         }
504
505         let win = this.focusedFrame;
506         let doc = win.document;
507         let sel = win.getSelection();
508         let { viewport } = DOM(win);
509
510         if (sel.rangeCount) {
511             var range = sel.getRangeAt(0);
512             if (visible(range).height > 0)
513                 return;
514
515             var { rect } = DOM(range);
516             var reverse = rect.bottom > viewport.bottom;
517
518             rect = { x: rect.left, y: 0, width: rect.width, height: win.innerHeight };
519         }
520         else {
521             let w = win.innerWidth;
522             rect = { x: w / 3, y: 0, width: w / 3, height: win.innerHeight };
523         }
524
525         var reduce = function (a, b) DOM(a).rect.top < DOM(b).rect.top ? a : b;
526         var dir = "forward";
527         var y = 0;
528         if (reverse) {
529             reduce = function (a, b) DOM(b).rect.bottom > DOM(a).rect.bottom ? b : a;
530             dir = "backward";
531             y = win.innerHeight - 1;
532         }
533
534         let ranges = getRanges(rect);
535         if (!ranges.length)
536             ranges = getRanges({ x: 0, y: y, width: win.innerWidth, height: 0 });
537
538         if (ranges.length) {
539             range = ranges.reduce(reduce);
540
541             if (range) {
542                 range.collapse(!reverse);
543                 sel.removeAllRanges();
544                 sel.addRange(range);
545                 do {
546                     if (visible(range).height > 0)
547                         break;
548
549                     var { startContainer, startOffset } = range;
550                     sel.modify("move", dir, "line");
551                     range = sel.getRangeAt(0);
552                 }
553                 while (startContainer != range.startContainer || startOffset != range.startOffset);
554
555                 sel.modify("move", reverse ? "forward" : "backward", "lineboundary");
556             }
557         }
558
559         if (!sel.rangeCount)
560             sel.collapse(doc.body || doc.querySelector("body") || doc.documentElement,
561                          0);
562     },
563
564     /**
565      * @property {nsISelection} The current document's normal selection.
566      */
567     get selection() this.win.getSelection(),
568
569     /**
570      * @property {nsISelectionController} The current document's selection
571      *     controller.
572      */
573     get selectionController() util.selectionController(this.focusedFrame),
574
575     /**
576      * Opens the appropriate context menu for *elem*.
577      *
578      * @param {Node} elem The context element.
579      */
580     openContextMenu: deprecated("DOM#contextmenu", function openContextMenu(elem) DOM(elem).contextmenu()),
581
582     /**
583      * Saves a page link to disk.
584      *
585      * @param {HTMLAnchorElement} elem The page link to save.
586      */
587     saveLink: function saveLink(elem) {
588         let { completion, dactyl, io } = this.modules;
589
590         let self = this;
591         let doc      = elem.ownerDocument;
592         let uri      = util.newURI(elem.href || elem.src, null, util.newURI(elem.baseURI));
593         let referrer = util.newURI(doc.documentURI, doc.characterSet);
594
595         try {
596             services.security.checkLoadURIWithPrincipal(doc.nodePrincipal, uri,
597                         services.security.STANDARD);
598
599             io.CommandFileMode(_("buffer.prompt.saveLink") + " ", {
600                 onSubmit: function (path) {
601                     let file = io.File(path);
602                     if (file.exists() && file.isDirectory())
603                         file.append(Buffer.getDefaultNames(elem)[0][0]);
604
605                     try {
606                         if (!file.exists())
607                             file.create(File.NORMAL_FILE_TYPE, octal(644));
608                     }
609                     catch (e) {
610                         util.assert(false, _("save.invalidDestination", e.name));
611                     }
612
613                     self.saveURI(uri, file);
614                 },
615
616                 completer: function (context) completion.savePage(context, elem)
617             }).open();
618         }
619         catch (e) {
620             dactyl.echoerr(e);
621         }
622     },
623
624     /**
625      * Saves the contents of a URI to disk.
626      *
627      * @param {nsIURI} uri The URI to save
628      * @param {nsIFile} file The file into which to write the result.
629      */
630     saveURI: function saveURI(uri, file, callback, self) {
631         var persist = services.Persist();
632         persist.persistFlags = persist.PERSIST_FLAGS_FROM_CACHE
633                              | persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
634
635         let window = this.topWindow;
636         let downloadListener = new window.DownloadListener(window,
637                 services.Transfer(uri, File(file).URI, "",
638                                   null, null, null, persist));
639
640         persist.progressListener = update(Object.create(downloadListener), {
641             onStateChange: util.wrapCallback(function onStateChange(progress, request, flags, status) {
642                 if (callback && (flags & Ci.nsIWebProgressListener.STATE_STOP) && status == 0)
643                     util.trapErrors(callback, self, uri, file, progress, request, flags, status);
644
645                 return onStateChange.superapply(this, arguments);
646             })
647         });
648
649         persist.saveURI(uri, null, null, null, null, file);
650     },
651
652     /**
653      * Scrolls the currently active element horizontally. See
654      * {@link Buffer.scrollHorizontal} for parameters.
655      */
656     scrollHorizontal: function scrollHorizontal(increment, number)
657         Buffer.scrollHorizontal(this.findScrollable(number, true), increment, number),
658
659     /**
660      * Scrolls the currently active element vertically. See
661      * {@link Buffer.scrollVertical} for parameters.
662      */
663     scrollVertical: function scrollVertical(increment, number)
664         Buffer.scrollVertical(this.findScrollable(number, false), increment, number),
665
666     /**
667      * Scrolls the currently active element to the given horizontal and
668      * vertical percentages. See {@link Buffer.scrollToPercent} for
669      * parameters.
670      */
671     scrollToPercent: function scrollToPercent(horizontal, vertical)
672         Buffer.scrollToPercent(this.findScrollable(0, vertical == null), horizontal, vertical),
673
674     /**
675      * Scrolls the currently active element to the given horizontal and
676      * vertical positions. See {@link Buffer.scrollToPosition} for
677      * parameters.
678      */
679     scrollToPosition: function scrollToPosition(horizontal, vertical)
680         Buffer.scrollToPosition(this.findScrollable(0, vertical == null), horizontal, vertical),
681
682     _scrollByScrollSize: function _scrollByScrollSize(count, direction) {
683         let { options } = this.modules;
684
685         if (count > 0)
686             options["scroll"] = count;
687         this.scrollByScrollSize(direction);
688     },
689
690     /**
691      * Scrolls the buffer vertically 'scroll' lines.
692      *
693      * @param {boolean} direction The direction to scroll. If true then
694      *     scroll up and if false scroll down.
695      * @param {number} count The multiple of 'scroll' lines to scroll.
696      * @optional
697      */
698     scrollByScrollSize: function scrollByScrollSize(direction, count) {
699         let { options } = this.modules;
700
701         direction = direction ? 1 : -1;
702         count = count || 1;
703
704         if (options["scroll"] > 0)
705             this.scrollVertical("lines", options["scroll"] * direction);
706         else
707             this.scrollVertical("pages", direction / 2);
708     },
709
710     /**
711      * Find the best candidate scrollable element for the given
712      * direction and orientation.
713      *
714      * @param {number} dir The direction in which the element must be
715      *   able to scroll. Negative numbers represent up or left, while
716      *   positive numbers represent down or right.
717      * @param {boolean} horizontal If true, look for horizontally
718      *   scrollable elements, otherwise look for vertically scrollable
719      *   elements.
720      */
721     findScrollable: function findScrollable(dir, horizontal) {
722         function find(elem) {
723             while (elem && !(elem instanceof Ci.nsIDOMElement) && elem.parentNode)
724                 elem = elem.parentNode;
725             for (; elem instanceof Ci.nsIDOMElement; elem = elem.parentNode)
726                 if (Buffer.isScrollable(elem, dir, horizontal))
727                     break;
728
729             return elem;
730         }
731
732         try {
733             var elem = this.focusedFrame.document.activeElement;
734             if (elem == elem.ownerDocument.body)
735                 elem = null;
736         }
737         catch (e) {}
738
739         try {
740             var sel = this.focusedFrame.getSelection();
741         }
742         catch (e) {}
743         if (!elem && sel && sel.rangeCount)
744             elem = sel.getRangeAt(0).startContainer;
745         if (elem)
746             elem = find(elem);
747
748         if (!(elem instanceof Ci.nsIDOMElement)) {
749             let doc = this.findScrollableWindow().document;
750             elem = find(doc.body || doc.getElementsByTagName("body")[0] ||
751                         doc.documentElement);
752         }
753         let doc = this.focusedFrame.document;
754         return util.assert(elem || doc.body || doc.documentElement);
755     },
756
757     /**
758      * Find the best candidate scrollable frame in the current buffer.
759      */
760     findScrollableWindow: function findScrollableWindow() {
761         let { document } = this.topWindow;
762
763         let win = document.commandDispatcher.focusedWindow;
764         if (win && (win.scrollMaxX > 0 || win.scrollMaxY > 0))
765             return win;
766
767         let win = this.focusedFrame;
768         if (win && (win.scrollMaxX > 0 || win.scrollMaxY > 0))
769             return win;
770
771         win = this.win;
772         if (win.scrollMaxX > 0 || win.scrollMaxY > 0)
773             return win;
774
775         for (let frame in array.iterValues(win.frames))
776             if (frame.scrollMaxX > 0 || frame.scrollMaxY > 0)
777                 return frame;
778
779         return win;
780     },
781
782     /**
783      * Finds the next visible element for the node path in 'jumptags'
784      * for *arg*.
785      *
786      * @param {string} arg The element in 'jumptags' to use for the search.
787      * @param {number} count The number of elements to jump.
788      *      @optional
789      * @param {boolean} reverse If true, search backwards. @optional
790      * @param {boolean} offScreen If true, include only off-screen elements. @optional
791      */
792     findJump: function findJump(arg, count, reverse, offScreen) {
793         let { marks, options } = this.modules;
794
795         const FUDGE = 10;
796
797         marks.push();
798
799         let path = options["jumptags"][arg];
800         util.assert(path, _("error.invalidArgument", arg));
801
802         let distance = reverse ? function (rect) -rect.top : function (rect) rect.top;
803         let elems = [[e, distance(e.getBoundingClientRect())] for (e in path.matcher(this.focusedFrame.document))]
804                         .filter(function (e) e[1] > FUDGE)
805                         .sort(function (a, b) a[1] - b[1])
806
807         if (offScreen && !reverse)
808             elems = elems.filter(function (e) e[1] > this, this.topWindow.innerHeight);
809
810         let idx = Math.min((count || 1) - 1, elems.length);
811         util.assert(idx in elems);
812
813         let elem = elems[idx][0];
814         elem.scrollIntoView(true);
815
816         let sel = elem.ownerDocument.defaultView.getSelection();
817         sel.removeAllRanges();
818         sel.addRange(RangeFind.endpoint(RangeFind.nodeRange(elem), true));
819     },
820
821     // TODO: allow callback for filtering out unwanted frames? User defined?
822     /**
823      * Shifts the focus to another frame within the buffer. Each buffer
824      * contains at least one frame.
825      *
826      * @param {number} count The number of frames to skip through. A negative
827      *     count skips backwards.
828      */
829     shiftFrameFocus: function shiftFrameFocus(count) {
830         if (!(this.doc instanceof Ci.nsIDOMHTMLDocument))
831             return;
832
833         let frames = this.allFrames();
834
835         if (frames.length == 0) // currently top is always included
836             return;
837
838         // remove all hidden frames
839         frames = frames.filter(function (frame) !(frame.document.body instanceof Ci.nsIDOMHTMLFrameSetElement))
840                        .filter(function (frame) !frame.frameElement ||
841             let (rect = frame.frameElement.getBoundingClientRect())
842                 rect.width && rect.height);
843
844         // find the currently focused frame index
845         let current = Math.max(0, frames.indexOf(this.focusedFrame));
846
847         // calculate the next frame to focus
848         let next = current + count;
849         if (next < 0 || next >= frames.length)
850             util.dactyl.beep();
851         next = Math.constrain(next, 0, frames.length - 1);
852
853         // focus next frame and scroll into view
854         DOM(frames[next]).focus();
855         if (frames[next] != this.win)
856             DOM(frames[next].frameElement).scrollIntoView();
857
858         // add the frame indicator
859         let doc = frames[next].document;
860         let indicator = DOM(<div highlight="FrameIndicator"/>, doc)
861                             .appendTo(doc.body || doc.documentElement || doc);
862
863         util.timeout(function () { indicator.remove(); }, 500);
864
865         // Doesn't unattach
866         //doc.body.setAttributeNS(NS.uri, "activeframe", "true");
867         //util.timeout(function () { doc.body.removeAttributeNS(NS.uri, "activeframe"); }, 500);
868     },
869
870     // similar to pageInfo
871     // TODO: print more useful information, just like the DOM inspector
872     /**
873      * Displays information about the specified element.
874      *
875      * @param {Node} elem The element to query.
876      */
877     showElementInfo: function showElementInfo(elem) {
878         let { dactyl } = this.modules;
879
880         XML.ignoreWhitespace = XML.prettyPrinting = false;
881         dactyl.echo(<><!--L-->Element:<br/>{util.objectToString(elem, true)}</>);
882     },
883
884     /**
885      * Displays information about the current buffer.
886      *
887      * @param {boolean} verbose Display more verbose information.
888      * @param {string} sections A string limiting the displayed sections.
889      * @default The value of 'pageinfo'.
890      */
891     showPageInfo: function showPageInfo(verbose, sections) {
892         let { commandline, dactyl, options } = this.modules;
893
894         let self = this;
895
896         // Ctrl-g single line output
897         if (!verbose) {
898             let file = this.win.location.pathname.split("/").pop() || _("buffer.noName");
899             let title = this.win.document.title || _("buffer.noTitle");
900
901             let info = template.map(
902                 (sections || options["pageinfo"])
903                     .map(function (opt) Buffer.pageInfo[opt].action.call(self)),
904                 function (res) res && iter(res).join(", ") || undefined,
905                 ", ");
906
907             if (bookmarkcache.isBookmarked(this.URL))
908                 info += ", " + _("buffer.bookmarked");
909
910             let pageInfoText = <>{file.quote()} [{info}] {title}</>;
911             dactyl.echo(pageInfoText, commandline.FORCE_SINGLELINE);
912             return;
913         }
914
915         let list = template.map(sections || options["pageinfo"], function (option) {
916             let { action, title } = Buffer.pageInfo[option];
917             return template.table(title, action.call(self, true));
918         }, <br/>);
919
920         commandline.commandOutput(list);
921     },
922
923     /**
924      * Stops loading and animations in the current content.
925      */
926     stop: function stop() {
927         let { config } = this.modules;
928
929         if (config.stop)
930             config.stop();
931         else
932             this.docShell.stop(this.docShell.STOP_ALL);
933     },
934
935     /**
936      * Opens a viewer to inspect the source of the currently selected
937      * range.
938      */
939     viewSelectionSource: function viewSelectionSource() {
940         // copied (and tuned somewhat) from browser.jar -> nsContextMenu.js
941         let { document, window } = this.topWindow;
942
943         let win = document.commandDispatcher.focusedWindow;
944         if (win == this.topWindow)
945             win = this.focusedFrame;
946
947         let charset = win ? "charset=" + win.document.characterSet : null;
948
949         window.openDialog("chrome://global/content/viewPartialSource.xul",
950                           "_blank", "scrollbars,resizable,chrome,dialog=no",
951                           null, charset, win.getSelection(), "selection");
952     },
953
954     /**
955      * Opens a viewer to inspect the source of the current buffer or the
956      * specified *url*. Either the default viewer or the configured external
957      * editor is used.
958      *
959      * @param {string|object|null} loc If a string, the URL of the source,
960      *      otherwise an object with some or all of the following properties:
961      *
962      *          url: The URL to view.
963      *          doc: The document to view.
964      *          line: The line to select.
965      *          column: The column to select.
966      *
967      *      If no URL is provided, the current document is used.
968      *  @default The current buffer.
969      * @param {boolean} useExternalEditor View the source in the external editor.
970      */
971     viewSource: function viewSource(loc, useExternalEditor) {
972         let { dactyl, editor, history, options } = this.modules;
973
974         let window = this.topWindow;
975
976         let doc = this.focusedFrame.document;
977
978         if (isObject(loc)) {
979             if (options.get("editor").has("line") || !loc.url)
980                 this.viewSourceExternally(loc.doc || loc.url || doc, loc);
981             else
982                 window.openDialog("chrome://global/content/viewSource.xul",
983                                   "_blank", "all,dialog=no",
984                                   loc.url, null, null, loc.line);
985         }
986         else {
987             if (useExternalEditor)
988                 this.viewSourceExternally(loc || doc);
989             else {
990                 let url = loc || doc.location.href;
991                 const PREFIX = "view-source:";
992                 if (url.indexOf(PREFIX) == 0)
993                     url = url.substr(PREFIX.length);
994                 else
995                     url = PREFIX + url;
996
997                 let sh = history.session;
998                 if (sh[sh.index].URI.spec == url)
999                     this.docShell.gotoIndex(sh.index);
1000                 else
1001                     dactyl.open(url, { hide: true });
1002             }
1003         }
1004     },
1005
1006     /**
1007      * Launches an editor to view the source of the given document. The
1008      * contents of the document are saved to a temporary local file and
1009      * removed when the editor returns. This function returns
1010      * immediately.
1011      *
1012      * @param {Document} doc The document to view.
1013      * @param {function|object} callback If a function, the callback to be
1014      *      called with two arguments: the nsIFile of the file, and temp, a
1015      *      boolean which is true if the file is temporary. Otherwise, an object
1016      *      with line and column properties used to determine where to open the
1017      *      source.
1018      *      @optional
1019      */
1020     viewSourceExternally: Class("viewSourceExternally",
1021         XPCOM([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), {
1022         init: function init(doc, callback) {
1023             this.callback = callable(callback) ? callback :
1024                 function (file, temp) {
1025                     let { editor } = overlay.activeModules;
1026
1027                     editor.editFileExternally(update({ file: file.path }, callback || {}),
1028                                               function () { temp && file.remove(false); });
1029                     return true;
1030                 };
1031
1032             let uri = isString(doc) ? util.newURI(doc) : util.newURI(doc.location.href);
1033             let ext = uri.fileExtension || "txt";
1034             if (doc.contentType)
1035                 ext = services.mime.getPrimaryExtension(doc.contentType, ext);
1036
1037             if (!isString(doc))
1038                 return io.withTempFiles(function (temp) {
1039                     let encoder = services.HtmlEncoder();
1040                     encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted);
1041                     temp.write(encoder.encodeToString(), ">");
1042                     return this.callback(temp, true);
1043                 }, this, true, ext);
1044
1045             let file = util.getFile(uri);
1046             if (file)
1047                 this.callback(file, false);
1048             else {
1049                 this.file = io.createTempFile();
1050                 var persist = services.Persist();
1051                 persist.persistFlags = persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
1052                 persist.progressListener = this;
1053                 persist.saveURI(uri, null, null, null, null, this.file);
1054             }
1055             return null;
1056         },
1057
1058         onStateChange: function onStateChange(progress, request, flags, status) {
1059             if ((flags & this.STATE_STOP) && status == 0) {
1060                 try {
1061                     var ok = this.callback(this.file, true);
1062                 }
1063                 finally {
1064                     if (ok !== true)
1065                         this.file.remove(false);
1066                 }
1067             }
1068             return 0;
1069         }
1070     }),
1071
1072     /**
1073      * Increases the zoom level of the current buffer.
1074      *
1075      * @param {number} steps The number of zoom levels to jump.
1076      * @param {boolean} fullZoom Whether to use full zoom or text zoom.
1077      */
1078     zoomIn: function zoomIn(steps, fullZoom) {
1079         this.bumpZoomLevel(steps, fullZoom);
1080     },
1081
1082     /**
1083      * Decreases the zoom level of the current buffer.
1084      *
1085      * @param {number} steps The number of zoom levels to jump.
1086      * @param {boolean} fullZoom Whether to use full zoom or text zoom.
1087      */
1088     zoomOut: function zoomOut(steps, fullZoom) {
1089         this.bumpZoomLevel(-steps, fullZoom);
1090     },
1091
1092     /**
1093      * Adjusts the page zoom of the current buffer to the given absolute
1094      * value.
1095      *
1096      * @param {number} value The new zoom value as a possibly fractional
1097      *   percentage of the page's natural size.
1098      * @param {boolean} fullZoom If true, zoom all content of the page,
1099      *   including raster images. If false, zoom only text. If omitted,
1100      *   use the current zoom function. @optional
1101      * @throws {FailedAssertion} if the given *value* is not within the
1102      *   closed range [Buffer.ZOOM_MIN, Buffer.ZOOM_MAX].
1103      */
1104     setZoom: function setZoom(value, fullZoom) {
1105         let { dactyl, statusline } = this.modules;
1106         let { ZoomManager } = this;
1107
1108         if (fullZoom === undefined)
1109             fullZoom = ZoomManager.useFullZoom;
1110         else
1111             ZoomManager.useFullZoom = fullZoom;
1112
1113         value /= 100;
1114         try {
1115             this.contentViewer.textZoom =  fullZoom ? 1 : value;
1116             this.contentViewer.fullZoom = !fullZoom ? 1 : value;
1117         }
1118         catch (e if e == Cr.NS_ERROR_ILLEGAL_VALUE) {
1119             return dactyl.echoerr(_("zoom.illegal"));
1120         }
1121
1122         if (services.has("contentPrefs") && !storage.privateMode
1123                 && prefs.get("browser.zoom.siteSpecific")) {
1124             services.contentPrefs[value != 1 ? "setPref" : "removePref"]
1125                 (this.uri, "browser.content.full-zoom", value);
1126             services.contentPrefs[value != 1 ? "setPref" : "removePref"]
1127                 (this.uri, "dactyl.content.full-zoom", fullZoom);
1128         }
1129
1130         statusline.updateZoomLevel();
1131     },
1132
1133     /**
1134      * Updates the zoom level of this buffer from a content preference.
1135      */
1136     updateZoom: util.wrapCallback(function updateZoom() {
1137         let self = this;
1138         let uri = this.uri;
1139
1140         if (services.has("contentPrefs") && prefs.get("browser.zoom.siteSpecific"))
1141             services.contentPrefs.getPref(uri, "dactyl.content.full-zoom", function (val) {
1142                 if (val != null && uri.equals(self.uri) && val != prefs.get("browser.zoom.full"))
1143                     [self.contentViewer.textZoom, self.contentViewer.fullZoom] =
1144                         [self.contentViewer.fullZoom, self.contentViewer.textZoom];
1145             });
1146     }),
1147
1148     /**
1149      * Adjusts the page zoom of the current buffer relative to the
1150      * current zoom level.
1151      *
1152      * @param {number} steps The integral number of natural fractions by which
1153      *     to adjust the current page zoom. If positive, the zoom level is
1154      *     increased, if negative it is decreased.
1155      * @param {boolean} fullZoom If true, zoom all content of the page,
1156      *     including raster images. If false, zoom only text. If omitted, use
1157      *     the current zoom function. @optional
1158      * @throws {FailedAssertion} if the buffer's zoom level is already at its
1159      *     extreme in the given direction.
1160      */
1161     bumpZoomLevel: function bumpZoomLevel(steps, fullZoom) {
1162         let { ZoomManager } = this;
1163
1164         if (fullZoom === undefined)
1165             fullZoom = ZoomManager.useFullZoom;
1166
1167         let values = ZoomManager.zoomValues;
1168         let cur = values.indexOf(ZoomManager.snap(this.zoomLevel / 100));
1169         let i = Math.constrain(cur + steps, 0, values.length - 1);
1170
1171         util.assert(i != cur || fullZoom != ZoomManager.useFullZoom);
1172
1173         this.setZoom(Math.round(values[i] * 100), fullZoom);
1174     },
1175
1176     getAllFrames: deprecated("buffer.allFrames", "allFrames"),
1177     scrollTop: deprecated("buffer.scrollToPercent", function scrollTop() this.scrollToPercent(null, 0)),
1178     scrollBottom: deprecated("buffer.scrollToPercent", function scrollBottom() this.scrollToPercent(null, 100)),
1179     scrollStart: deprecated("buffer.scrollToPercent", function scrollStart() this.scrollToPercent(0, null)),
1180     scrollEnd: deprecated("buffer.scrollToPercent", function scrollEnd() this.scrollToPercent(100, null)),
1181     scrollColumns: deprecated("buffer.scrollHorizontal", function scrollColumns(cols) this.scrollHorizontal("columns", cols)),
1182     scrollPages: deprecated("buffer.scrollHorizontal", function scrollPages(pages) this.scrollVertical("pages", pages)),
1183     scrollTo: deprecated("Buffer.scrollTo", function scrollTo(x, y) this.win.scrollTo(x, y)),
1184     textZoom: deprecated("buffer.zoomValue/buffer.fullZoom", function textZoom() this.contentViewer.markupDocumentViewer.textZoom * 100)
1185 }, {
1186     PageInfo: Struct("PageInfo", "name", "title", "action")
1187                         .localize("title"),
1188
1189     pageInfo: {},
1190
1191     /**
1192      * Adds a new section to the page information output.
1193      *
1194      * @param {string} option The section's value in 'pageinfo'.
1195      * @param {string} title The heading for this section's
1196      *     output.
1197      * @param {function} func The function to generate this
1198      *     section's output.
1199      */
1200     addPageInfoSection: function addPageInfoSection(option, title, func) {
1201         this.pageInfo[option] = Buffer.PageInfo(option, title, func);
1202     },
1203
1204     Scrollable: function Scrollable(elem) {
1205         if (elem instanceof Ci.nsIDOMElement)
1206             return elem;
1207         if (isinstance(elem, [Ci.nsIDOMWindow, Ci.nsIDOMDocument]))
1208             return {
1209                 __proto__: elem.documentElement || elem.ownerDocument.documentElement,
1210
1211                 win: elem.defaultView || elem.ownerDocument.defaultView,
1212
1213                 get clientWidth() this.win.innerWidth,
1214                 get clientHeight() this.win.innerHeight,
1215
1216                 get scrollWidth() this.win.scrollMaxX + this.win.innerWidth,
1217                 get scrollHeight() this.win.scrollMaxY + this.win.innerHeight,
1218
1219                 get scrollLeft() this.win.scrollX,
1220                 set scrollLeft(val) { this.win.scrollTo(val, this.win.scrollY) },
1221
1222                 get scrollTop() this.win.scrollY,
1223                 set scrollTop(val) { this.win.scrollTo(this.win.scrollX, val) }
1224             };
1225         return elem;
1226     },
1227
1228     get ZOOM_MIN() prefs.get("zoom.minPercent"),
1229     get ZOOM_MAX() prefs.get("zoom.maxPercent"),
1230
1231     setZoom: deprecated("buffer.setZoom", function setZoom()
1232                         let ({ buffer } = overlay.activeModules) buffer.setZoom.apply(buffer, arguments)),
1233     bumpZoomLevel: deprecated("buffer.bumpZoomLevel", function bumpZoomLevel()
1234                               let ({ buffer } = overlay.activeModules) buffer.bumpZoomLevel.apply(buffer, arguments)),
1235
1236     /**
1237      * Returns the currently selected word in *win*. If the selection is
1238      * null, it tries to guess the word that the caret is positioned in.
1239      *
1240      * @returns {string}
1241      */
1242     currentWord: function currentWord(win, select) {
1243         let { Editor, options } = Buffer(win).modules;
1244
1245         let selection = win.getSelection();
1246         if (selection.rangeCount == 0)
1247             return "";
1248
1249         let range = selection.getRangeAt(0).cloneRange();
1250         if (range.collapsed) {
1251             let re = options.get("iskeyword").regexp;
1252             Editor.extendRange(range, true,  re, true);
1253             Editor.extendRange(range, false, re, true);
1254         }
1255         if (select) {
1256             selection.removeAllRanges();
1257             selection.addRange(range);
1258         }
1259         return DOM.stringify(range);
1260     },
1261
1262     getDefaultNames: function getDefaultNames(node) {
1263         let url = node.href || node.src || node.documentURI;
1264         let currExt = url.replace(/^.*?(?:\.([a-z0-9]+))?$/i, "$1").toLowerCase();
1265
1266         let ext = "";
1267         if (isinstance(node, [Ci.nsIDOMDocument,
1268                               Ci.nsIDOMHTMLImageElement])) {
1269             let type = node.contentType || node.QueryInterface(Ci.nsIImageLoadingContent)
1270                                                .getRequest(0).mimeType;
1271
1272             if (type === "text/plain")
1273                 ext = "." + (currExt || "txt");
1274             else
1275                 ext = "." + services.mime.getPrimaryExtension(type, currExt);
1276         }
1277         else if (currExt)
1278             ext = "." + currExt;
1279
1280         let re = ext ? RegExp("(\\." + currExt + ")?$", "i") : /$/;
1281
1282         var names = [];
1283         if (node.title)
1284             names.push([node.title,
1285                        _("buffer.save.pageName")]);
1286
1287         if (node.alt)
1288             names.push([node.alt,
1289                        _("buffer.save.altText")]);
1290
1291         if (!isinstance(node, Ci.nsIDOMDocument) && node.textContent)
1292             names.push([node.textContent,
1293                        _("buffer.save.linkText")]);
1294
1295         names.push([decodeURIComponent(url.replace(/.*?([^\/]*)\/*$/, "$1")),
1296                     _("buffer.save.filename")]);
1297
1298         return names.filter(function ([leaf, title]) leaf)
1299                     .map(function ([leaf, title]) [leaf.replace(config.OS.illegalCharacters, encodeURIComponent)
1300                                                        .replace(re, ext), title]);
1301     },
1302
1303     findScrollableWindow: deprecated("buffer.findScrollableWindow", function findScrollableWindow()
1304                                      let ({ buffer } = overlay.activeModules) buffer.findScrollableWindow.apply(buffer, arguments)),
1305     findScrollable: deprecated("buffer.findScrollable", function findScrollable()
1306                                let ({ buffer } = overlay.activeModules) buffer.findScrollable.apply(buffer, arguments)),
1307
1308     isScrollable: function isScrollable(elem, dir, horizontal) {
1309         if (!DOM(elem).isScrollable(horizontal ? "horizontal" : "vertical"))
1310             return false;
1311
1312         return this.canScroll(elem, dir, horizontal);
1313     },
1314
1315     canScroll: function canScroll(elem, dir, horizontal) {
1316         let pos = "scrollTop", size = "clientHeight", max = "scrollHeight", layoutSize = "offsetHeight",
1317             overflow = "overflowX", border1 = "borderTopWidth", border2 = "borderBottomWidth";
1318         if (horizontal)
1319             pos = "scrollLeft", size = "clientWidth", max = "scrollWidth", layoutSize = "offsetWidth",
1320             overflow = "overflowX", border1 = "borderLeftWidth", border2 = "borderRightWidth";
1321
1322         let style = DOM(elem).style;
1323         let borderSize = Math.round(parseFloat(style[border1]) + parseFloat(style[border2]));
1324         let realSize = elem[size];
1325
1326         // Stupid Gecko eccentricities. May fail for quirks mode documents.
1327         if (elem[size] + borderSize == elem[max] || elem[size] == 0) // Stupid, fallible heuristic.
1328             return false;
1329
1330         if (style[overflow] == "hidden")
1331             realSize += borderSize;
1332         return dir < 0 && elem[pos] > 0 || dir > 0 && elem[pos] + realSize < elem[max] || !dir && realSize < elem[max];
1333     },
1334
1335     /**
1336      * Scroll the contents of the given element to the absolute *left*
1337      * and *top* pixel offsets.
1338      *
1339      * @param {Element} elem The element to scroll.
1340      * @param {number|null} left The left absolute pixel offset. If
1341      *      null, to not alter the horizontal scroll offset.
1342      * @param {number|null} top The top absolute pixel offset. If
1343      *      null, to not alter the vertical scroll offset.
1344      * @param {string} reason The reason for the scroll event. See
1345      *      {@link marks.push}. @optional
1346      */
1347     scrollTo: function scrollTo(elem, left, top, reason) {
1348         let doc = elem.ownerDocument || elem.document || elem;
1349
1350         let { buffer, marks, options } = util.topWindow(doc.defaultView).dactyl.modules;
1351
1352         if (~[elem, elem.document, elem.ownerDocument].indexOf(buffer.focusedFrame.document))
1353             marks.push(reason);
1354
1355         if (options["scrollsteps"] > 1)
1356             return this.smoothScrollTo(elem, left, top);
1357
1358         elem = Buffer.Scrollable(elem);
1359         if (left != null)
1360             elem.scrollLeft = left;
1361         if (top != null)
1362             elem.scrollTop = top;
1363     },
1364
1365     /**
1366      * Like scrollTo, but scrolls more smoothly and does not update
1367      * marks.
1368      */
1369     smoothScrollTo: function smoothScrollTo(node, x, y) {
1370         let { options } = overlay.activeModules;
1371
1372         let time = options["scrolltime"];
1373         let steps = options["scrollsteps"];
1374
1375         let elem = Buffer.Scrollable(node);
1376
1377         if (node.dactylScrollTimer)
1378             node.dactylScrollTimer.cancel();
1379
1380         if (x == null)
1381             x = elem.scrollLeft;
1382         if (y == null)
1383             y = elem.scrollTop;
1384
1385         x = node.dactylScrollDestX = Math.min(x, elem.scrollWidth  - elem.clientWidth);
1386         y = node.dactylScrollDestY = Math.min(y, elem.scrollHeight - elem.clientHeight);
1387         let [startX, startY] = [elem.scrollLeft, elem.scrollTop];
1388         let n = 0;
1389         (function next() {
1390             if (n++ === steps) {
1391                 elem.scrollLeft = x;
1392                 elem.scrollTop  = y;
1393                 delete node.dactylScrollDestX;
1394                 delete node.dactylScrollDestY;
1395             }
1396             else {
1397                 elem.scrollLeft = startX + (x - startX) / steps * n;
1398                 elem.scrollTop  = startY + (y - startY) / steps * n;
1399                 node.dactylScrollTimer = util.timeout(next, time / steps);
1400             }
1401         }).call(this);
1402     },
1403
1404     /**
1405      * Scrolls the currently given element horizontally.
1406      *
1407      * @param {Element} elem The element to scroll.
1408      * @param {string} unit The increment by which to scroll.
1409      *   Possible values are: "columns", "pages"
1410      * @param {number} number The possibly fractional number of
1411      *   increments to scroll. Positive values scroll to the right while
1412      *   negative values scroll to the left.
1413      * @throws {FailedAssertion} if scrolling is not possible in the
1414      *   given direction.
1415      */
1416     scrollHorizontal: function scrollHorizontal(node, unit, number) {
1417         let fontSize = parseInt(DOM(node).style.fontSize);
1418
1419         let elem = Buffer.Scrollable(node);
1420         let increment;
1421         if (unit == "columns")
1422             increment = fontSize; // Good enough, I suppose.
1423         else if (unit == "pages")
1424             increment = elem.clientWidth - fontSize;
1425         else
1426             throw Error();
1427
1428         util.assert(number < 0 ? elem.scrollLeft > 0 : elem.scrollLeft < elem.scrollWidth - elem.clientWidth);
1429
1430         let left = node.dactylScrollDestX !== undefined ? node.dactylScrollDestX : elem.scrollLeft;
1431         node.dactylScrollDestX = undefined;
1432
1433         Buffer.scrollTo(node, left + number * increment, null, "h-" + unit);
1434     },
1435
1436     /**
1437      * Scrolls the given element vertically.
1438      *
1439      * @param {Element} elem The element to scroll.
1440      * @param {string} unit The increment by which to scroll.
1441      *   Possible values are: "lines", "pages"
1442      * @param {number} number The possibly fractional number of
1443      *   increments to scroll. Positive values scroll upward while
1444      *   negative values scroll downward.
1445      * @throws {FailedAssertion} if scrolling is not possible in the
1446      *   given direction.
1447      */
1448     scrollVertical: function scrollVertical(node, unit, number) {
1449         let fontSize = parseInt(DOM(node).style.lineHeight);
1450
1451         let elem = Buffer.Scrollable(node);
1452         let increment;
1453         if (unit == "lines")
1454             increment = fontSize;
1455         else if (unit == "pages")
1456             increment = elem.clientHeight - fontSize;
1457         else
1458             throw Error();
1459
1460         util.assert(number < 0 ? elem.scrollTop > 0 : elem.scrollTop < elem.scrollHeight - elem.clientHeight);
1461
1462         let top = node.dactylScrollDestY !== undefined ? node.dactylScrollDestY : elem.scrollTop;
1463         node.dactylScrollDestY = undefined;
1464
1465         Buffer.scrollTo(node, null, top + number * increment, "v-" + unit);
1466     },
1467
1468     /**
1469      * Scrolls the currently active element to the given horizontal and
1470      * vertical percentages.
1471      *
1472      * @param {Element} elem The element to scroll.
1473      * @param {number|null} horizontal The possibly fractional
1474      *   percentage of the current viewport width to scroll to. If null,
1475      *   do not scroll horizontally.
1476      * @param {number|null} vertical The possibly fractional percentage
1477      *   of the current viewport height to scroll to. If null, do not
1478      *   scroll vertically.
1479      */
1480     scrollToPercent: function scrollToPercent(node, horizontal, vertical) {
1481         let elem = Buffer.Scrollable(node);
1482         Buffer.scrollTo(node,
1483                         horizontal == null ? null
1484                                            : (elem.scrollWidth - elem.clientWidth) * (horizontal / 100),
1485                         vertical   == null ? null
1486                                            : (elem.scrollHeight - elem.clientHeight) * (vertical / 100));
1487     },
1488
1489     /**
1490      * Scrolls the currently active element to the given horizontal and
1491      * vertical position.
1492      *
1493      * @param {Element} elem The element to scroll.
1494      * @param {number|null} horizontal The possibly fractional
1495      *      line ordinal to scroll to.
1496      * @param {number|null} vertical The possibly fractional
1497      *      column ordinal to scroll to.
1498      */
1499     scrollToPosition: function scrollToPosition(elem, horizontal, vertical) {
1500         let style = DOM(elem.body || elem).style;
1501         Buffer.scrollTo(elem,
1502                         horizontal == null ? null :
1503                         horizontal == 0    ? 0    : this._exWidth(elem) * horizontal,
1504                         vertical   == null ? null : parseFloat(style.lineHeight) * vertical);
1505     },
1506
1507     /**
1508      * Returns the current scroll position as understood by
1509      * {@link #scrollToPosition}.
1510      *
1511      * @param {Element} elem The element to scroll.
1512      */
1513     getScrollPosition: function getPosition(node) {
1514         let style = DOM(node.body || node).style;
1515
1516         let elem = Buffer.Scrollable(node);
1517         return {
1518             x: elem.scrollLeft && elem.scrollLeft / this._exWidth(node),
1519             y: elem.scrollTop / parseFloat(style.lineHeight)
1520         }
1521     },
1522
1523     _exWidth: function _exWidth(elem) {
1524         try {
1525             let div = DOM(<elem style="width: 1ex !important; position: absolute !important; padding: 0 !important; display: block;"/>,
1526                           elem.ownerDocument).appendTo(elem.body || elem);
1527             try {
1528                 return parseFloat(div.style.width);
1529             }
1530             finally {
1531                 div.remove();
1532             }
1533         }
1534         catch (e) {
1535             return parseFloat(DOM(elem).fontSize) / 1.618;
1536         }
1537     },
1538
1539     openUploadPrompt: function openUploadPrompt(elem) {
1540         let { io } = overlay.activeModules;
1541
1542         io.CommandFileMode(_("buffer.prompt.uploadFile") + " ", {
1543             onSubmit: function onSubmit(path) {
1544                 let file = io.File(path);
1545                 util.assert(file.exists());
1546
1547                 DOM(elem).val(file.path).change();
1548             }
1549         }).open(elem.value);
1550     }
1551 }, {
1552     init: function init(dactyl, modules, window) {
1553         init.superapply(this, arguments);
1554
1555         dactyl.commands["buffer.viewSource"] = function (event) {
1556             let elem = event.originalTarget;
1557             let obj = { url: elem.getAttribute("href"), line: Number(elem.getAttribute("line")) };
1558             if (elem.hasAttribute("column"))
1559                 obj.column = elem.getAttribute("column");
1560
1561             modules.buffer.viewSource(obj);
1562         };
1563     },
1564     commands: function initCommands(dactyl, modules, window) {
1565         let { buffer, commands, config, options } = modules;
1566
1567         commands.add(["frameo[nly]"],
1568             "Show only the current frame's page",
1569             function (args) {
1570                 dactyl.open(buffer.focusedFrame.location.href);
1571             },
1572             { argCount: "0" });
1573
1574         commands.add(["ha[rdcopy]"],
1575             "Print current document",
1576             function (args) {
1577                 let arg = args[0];
1578
1579                 // FIXME: arg handling is a bit of a mess, check for filename
1580                 dactyl.assert(!arg || arg[0] == ">" && !config.OS.isWindows,
1581                               _("error.trailingCharacters"));
1582
1583                 const PRINTER = "PostScript/default";
1584                 const BRANCH  = "print.printer_" + PRINTER + ".";
1585
1586                 prefs.withContext(function () {
1587                     if (arg) {
1588                         prefs.set("print.print_printer", PRINTER);
1589
1590                         prefs.set(   "print.print_to_file", true);
1591                         prefs.set(BRANCH + "print_to_file", true);
1592
1593                         prefs.set(   "print.print_to_filename", io.File(arg.substr(1)).path);
1594                         prefs.set(BRANCH + "print_to_filename", io.File(arg.substr(1)).path);
1595
1596                         dactyl.echomsg(_("print.toFile", arg.substr(1)));
1597                     }
1598                     else
1599                         dactyl.echomsg(_("print.sending"));
1600
1601                     prefs.set("print.always_print_silent", args.bang);
1602                     prefs.set("print.show_print_progress", !args.bang);
1603
1604                     config.browser.contentWindow.print();
1605                 });
1606
1607                 if (arg)
1608                     dactyl.echomsg(_("print.printed", arg.substr(1)));
1609                 else
1610                     dactyl.echomsg(_("print.sent"));
1611             },
1612             {
1613                 argCount: "?",
1614                 bang: true,
1615                 literal: 0
1616             });
1617
1618         commands.add(["pa[geinfo]"],
1619             "Show various page information",
1620             function (args) {
1621                 let arg = args[0];
1622                 let opt = options.get("pageinfo");
1623
1624                 dactyl.assert(!arg || opt.validator(opt.parse(arg)),
1625                               _("error.invalidArgument", arg));
1626                 buffer.showPageInfo(true, arg);
1627             },
1628             {
1629                 argCount: "?",
1630                 completer: function (context) {
1631                     modules.completion.optionValue(context, "pageinfo", "+", "");
1632                     context.title = ["Page Info"];
1633                 }
1634             });
1635
1636         commands.add(["pagest[yle]", "pas"],
1637             "Select the author style sheet to apply",
1638             function (args) {
1639                 let arg = args[0] || "";
1640
1641                 let titles = buffer.alternateStyleSheets.map(function (stylesheet) stylesheet.title);
1642
1643                 dactyl.assert(!arg || titles.indexOf(arg) >= 0,
1644                               _("error.invalidArgument", arg));
1645
1646                 if (options["usermode"])
1647                     options["usermode"] = false;
1648
1649                 window.stylesheetSwitchAll(buffer.focusedFrame, arg);
1650             },
1651             {
1652                 argCount: "?",
1653                 completer: function (context) modules.completion.alternateStyleSheet(context),
1654                 literal: 0
1655             });
1656
1657         commands.add(["re[load]"],
1658             "Reload the current web page",
1659             function (args) { modules.tabs.reload(config.browser.mCurrentTab, args.bang); },
1660             {
1661                 argCount: "0",
1662                 bang: true
1663             });
1664
1665         // TODO: we're prompted if download.useDownloadDir isn't set and no arg specified - intentional?
1666         commands.add(["sav[eas]", "w[rite]"],
1667             "Save current document to disk",
1668             function (args) {
1669                 let { commandline, io } = modules;
1670                 let { doc, win } = buffer;
1671
1672                 let chosenData = null;
1673                 let filename = args[0];
1674
1675                 let command = commandline.command;
1676                 if (filename) {
1677                     if (filename[0] == "!")
1678                         return buffer.viewSourceExternally(buffer.focusedFrame.document,
1679                             function (file) {
1680                                 let output = io.system(filename.substr(1), file);
1681                                 commandline.command = command;
1682                                 commandline.commandOutput(<span highlight="CmdOutput">{output}</span>);
1683                             });
1684
1685                     if (/^>>/.test(filename)) {
1686                         let file = io.File(filename.replace(/^>>\s*/, ""));
1687                         dactyl.assert(args.bang || file.exists() && file.isWritable(),
1688                                       _("io.notWriteable", file.path.quote()));
1689
1690                         return buffer.viewSourceExternally(buffer.focusedFrame.document,
1691                             function (tmpFile) {
1692                                 try {
1693                                     file.write(tmpFile, ">>");
1694                                 }
1695                                 catch (e) {
1696                                     dactyl.echoerr(_("io.notWriteable", file.path.quote()));
1697                                 }
1698                             });
1699                     }
1700
1701                     let file = io.File(filename);
1702
1703                     if (filename.substr(-1) === File.PATH_SEP || file.exists() && file.isDirectory())
1704                         file.append(Buffer.getDefaultNames(doc)[0][0]);
1705
1706                     dactyl.assert(args.bang || !file.exists(), _("io.exists"));
1707
1708                     chosenData = { file: file, uri: util.newURI(doc.location.href) };
1709                 }
1710
1711                 // if browser.download.useDownloadDir = false then the "Save As"
1712                 // dialog is used with this as the default directory
1713                 // TODO: if we're going to do this shouldn't it be done in setCWD or the value restored?
1714                 prefs.set("browser.download.lastDir", io.cwd.path);
1715
1716                 try {
1717                     var contentDisposition = win.QueryInterface(Ci.nsIInterfaceRequestor)
1718                                                 .getInterface(Ci.nsIDOMWindowUtils)
1719                                                 .getDocumentMetadata("content-disposition");
1720                 }
1721                 catch (e) {}
1722
1723                 window.internalSave(doc.location.href, doc, null, contentDisposition,
1724                                     doc.contentType, false, null, chosenData,
1725                                     doc.referrer ? window.makeURI(doc.referrer) : null,
1726                                     true);
1727             },
1728             {
1729                 argCount: "?",
1730                 bang: true,
1731                 completer: function (context) {
1732                     let { buffer, completion } = modules;
1733
1734                     if (context.filter[0] == "!")
1735                         return;
1736                     if (/^>>/.test(context.filter))
1737                         context.advance(/^>>\s*/.exec(context.filter)[0].length);
1738
1739                     completion.savePage(context, buffer.doc);
1740                     context.fork("file", 0, completion, "file");
1741                 },
1742                 literal: 0
1743             });
1744
1745         commands.add(["st[op]"],
1746             "Stop loading the current web page",
1747             function () { buffer.stop(); },
1748             { argCount: "0" });
1749
1750         commands.add(["vie[wsource]"],
1751             "View source code of current document",
1752             function (args) { buffer.viewSource(args[0], args.bang); },
1753             {
1754                 argCount: "?",
1755                 bang: true,
1756                 completer: function (context) modules.completion.url(context, "bhf")
1757             });
1758
1759         commands.add(["zo[om]"],
1760             "Set zoom value of current web page",
1761             function (args) {
1762                 let arg = args[0];
1763                 let level;
1764
1765                 if (!arg)
1766                     level = 100;
1767                 else if (/^\d+$/.test(arg))
1768                     level = parseInt(arg, 10);
1769                 else if (/^[+-]\d+$/.test(arg))
1770                     level = Math.round(buffer.zoomLevel + parseInt(arg, 10));
1771                 else
1772                     dactyl.assert(false, _("error.trailingCharacters"));
1773
1774                 buffer.setZoom(level, args.bang);
1775             },
1776             {
1777                 argCount: "?",
1778                 bang: true
1779             });
1780     },
1781     completion: function initCompletion(dactyl, modules, window) {
1782         let { CompletionContext, buffer, completion } = modules;
1783
1784         completion.alternateStyleSheet = function alternateStylesheet(context) {
1785             context.title = ["Stylesheet", "Location"];
1786
1787             // unify split style sheets
1788             let styles = iter([s.title, []] for (s in values(buffer.alternateStyleSheets))).toObject();
1789
1790             buffer.alternateStyleSheets.forEach(function (style) {
1791                 styles[style.title].push(style.href || _("style.inline"));
1792             });
1793
1794             context.completions = [[title, href.join(", ")] for ([title, href] in Iterator(styles))];
1795         };
1796
1797         completion.savePage = function savePage(context, node) {
1798             context.fork("generated", context.filter.replace(/[^/]*$/, "").length,
1799                          this, function (context) {
1800                 context.generate = function () {
1801                     this.incomplete = true;
1802                     this.completions = Buffer.getDefaultNames(node);
1803                     util.httpGet(node.href || node.src || node.documentURI, {
1804                         method: "HEAD",
1805                         callback: function callback(xhr) {
1806                             context.incomplete = false;
1807                             try {
1808                                 if (/filename="(.*?)"/.test(xhr.getResponseHeader("Content-Disposition")))
1809                                     context.completions.push([decodeURIComponent(RegExp.$1), _("buffer.save.suggested")]);
1810                             }
1811                             finally {
1812                                 context.completions = context.completions.slice();
1813                             }
1814                         },
1815                         notificationCallbacks: Class(XPCOM([Ci.nsIChannelEventSink, Ci.nsIInterfaceRequestor]), {
1816                             getInterface: function getInterface(iid) this.QueryInterface(iid),
1817
1818                             asyncOnChannelRedirect: function (oldChannel, newChannel, flags, callback) {
1819                                 if (newChannel instanceof Ci.nsIHttpChannel)
1820                                     newChannel.requestMethod = "HEAD";
1821                                 callback.onRedirectVerifyCallback(Cr.NS_OK);
1822                             }
1823                         })()
1824                     });
1825                 };
1826             });
1827         };
1828     },
1829     events: function initEvents(dactyl, modules, window) {
1830         let { buffer, config, events } = modules;
1831
1832         events.listen(config.browser, "scroll", buffer.closure._updateBufferPosition, false);
1833     },
1834     mappings: function initMappings(dactyl, modules, window) {
1835         let { Editor, Events, buffer, editor, events, ex, mappings, modes, options, tabs } = modules;
1836
1837         mappings.add([modes.NORMAL],
1838             ["y", "<yank-location>"], "Yank current location to the clipboard",
1839             function () {
1840                 let { doc, uri } = buffer;
1841                 if (uri instanceof Ci.nsIURL)
1842                     uri.query = uri.query.replace(/(?:^|&)utm_[^&]+/g, "")
1843                                          .replace(/^&/, "");
1844
1845                 let link = DOM("link[href][rev=canonical], link[href][rel=shortlink]", doc);
1846                 let url = link.length && options.get("yankshort").getKey(uri) ? link.attr("href") : uri.spec;
1847                 dactyl.clipboardWrite(url, true);
1848             });
1849
1850         mappings.add([modes.NORMAL],
1851             ["<C-a>", "<increment-url-path>"], "Increment last number in URL",
1852             function (args) { buffer.incrementURL(Math.max(args.count, 1)); },
1853             { count: true });
1854
1855         mappings.add([modes.NORMAL],
1856             ["<C-x>", "<decrement-url-path>"], "Decrement last number in URL",
1857             function (args) { buffer.incrementURL(-Math.max(args.count, 1)); },
1858             { count: true });
1859
1860         mappings.add([modes.NORMAL], ["gu", "<open-parent-path>"],
1861             "Go to parent directory",
1862             function (args) { buffer.climbUrlPath(Math.max(args.count, 1)); },
1863             { count: true });
1864
1865         mappings.add([modes.NORMAL], ["gU", "<open-root-path>"],
1866             "Go to the root of the website",
1867             function () { buffer.climbUrlPath(-1); });
1868
1869         mappings.add([modes.COMMAND], [".", "<repeat-key>"],
1870             "Repeat the last key event",
1871             function (args) {
1872                 if (mappings.repeat) {
1873                     for (let i in util.interruptibleRange(0, Math.max(args.count, 1), 100))
1874                         mappings.repeat();
1875                 }
1876             },
1877             { count: true });
1878
1879         mappings.add([modes.NORMAL], ["i", "<Insert>"],
1880             "Start Caret mode",
1881             function () { modes.push(modes.CARET); });
1882
1883         mappings.add([modes.NORMAL], ["<C-c>", "<stop-load>"],
1884             "Stop loading the current web page",
1885             function () { ex.stop(); });
1886
1887         // scrolling
1888         mappings.add([modes.NORMAL], ["j", "<Down>", "<C-e>", "<scroll-down-line>"],
1889             "Scroll document down",
1890             function (args) { buffer.scrollVertical("lines", Math.max(args.count, 1)); },
1891             { count: true });
1892
1893         mappings.add([modes.NORMAL], ["k", "<Up>", "<C-y>", "<scroll-up-line>"],
1894             "Scroll document up",
1895             function (args) { buffer.scrollVertical("lines", -Math.max(args.count, 1)); },
1896             { count: true });
1897
1898         mappings.add([modes.COMMAND], dactyl.has("mail") ? ["h", "<scroll-left-column>"] : ["h", "<Left>", "<scroll-left-column>"],
1899             "Scroll document to the left",
1900             function (args) { buffer.scrollHorizontal("columns", -Math.max(args.count, 1)); },
1901             { count: true });
1902
1903         mappings.add([modes.NORMAL], dactyl.has("mail") ? ["l", "<scroll-right-column>"] : ["l", "<Right>", "<scroll-right-column>"],
1904             "Scroll document to the right",
1905             function (args) { buffer.scrollHorizontal("columns", Math.max(args.count, 1)); },
1906             { count: true });
1907
1908         mappings.add([modes.NORMAL], ["0", "^", "<scroll-begin>"],
1909             "Scroll to the absolute left of the document",
1910             function () { buffer.scrollToPercent(0, null); });
1911
1912         mappings.add([modes.NORMAL], ["$", "<scroll-end>"],
1913             "Scroll to the absolute right of the document",
1914             function () { buffer.scrollToPercent(100, null); });
1915
1916         mappings.add([modes.NORMAL], ["gg", "<Home>", "<scroll-top>"],
1917             "Go to the top of the document",
1918             function (args) { buffer.scrollToPercent(null, args.count != null ? args.count : 0); },
1919             { count: true });
1920
1921         mappings.add([modes.NORMAL], ["G", "<End>", "<scroll-bottom>"],
1922             "Go to the end of the document",
1923             function (args) {
1924                 if (args.count)
1925                     var elem = options.get("linenumbers")
1926                                       .getLine(buffer.focusedFrame.document,
1927                                                args.count);
1928                 if (elem)
1929                     elem.scrollIntoView(true);
1930                 else if (args.count)
1931                     buffer.scrollToPosition(null, args.count);
1932                 else
1933                     buffer.scrollToPercent(null, 100);
1934             },
1935             { count: true });
1936
1937         mappings.add([modes.NORMAL], ["%", "<scroll-percent>"],
1938             "Scroll to {count} percent of the document",
1939             function (args) {
1940                 dactyl.assert(args.count > 0 && args.count <= 100);
1941                 buffer.scrollToPercent(null, args.count);
1942             },
1943             { count: true });
1944
1945         mappings.add([modes.NORMAL], ["<C-d>", "<scroll-down>"],
1946             "Scroll window downwards in the buffer",
1947             function (args) { buffer._scrollByScrollSize(args.count, true); },
1948             { count: true });
1949
1950         mappings.add([modes.NORMAL], ["<C-u>", "<scroll-up>"],
1951             "Scroll window upwards in the buffer",
1952             function (args) { buffer._scrollByScrollSize(args.count, false); },
1953             { count: true });
1954
1955         mappings.add([modes.NORMAL], ["<C-b>", "<PageUp>", "<S-Space>", "<scroll-up-page>"],
1956             "Scroll up a full page",
1957             function (args) { buffer.scrollVertical("pages", -Math.max(args.count, 1)); },
1958             { count: true });
1959
1960         mappings.add([modes.NORMAL], ["<Space>"],
1961             "Scroll down a full page",
1962             function (args) {
1963                 if (isinstance((services.focus.focusedWindow || buffer.win).document.activeElement,
1964                                [Ci.nsIDOMHTMLInputElement,
1965                                 Ci.nsIDOMHTMLButtonElement,
1966                                 Ci.nsIDOMXULButtonElement]))
1967                     return Events.PASS;
1968
1969                 buffer.scrollVertical("pages", Math.max(args.count, 1));
1970             },
1971             { count: true });
1972
1973         mappings.add([modes.NORMAL], ["<C-f>", "<PageDown>", "<scroll-down-page>"],
1974             "Scroll down a full page",
1975             function (args) { buffer.scrollVertical("pages", Math.max(args.count, 1)); },
1976             { count: true });
1977
1978         mappings.add([modes.NORMAL], ["]f", "<previous-frame>"],
1979             "Focus next frame",
1980             function (args) { buffer.shiftFrameFocus(Math.max(args.count, 1)); },
1981             { count: true });
1982
1983         mappings.add([modes.NORMAL], ["[f", "<next-frame>"],
1984             "Focus previous frame",
1985             function (args) { buffer.shiftFrameFocus(-Math.max(args.count, 1)); },
1986             { count: true });
1987
1988         mappings.add([modes.NORMAL], ["["],
1989             "Jump to the previous element as defined by 'jumptags'",
1990             function (args) { buffer.findJump(args.arg, args.count, true); },
1991             { arg: true, count: true });
1992
1993         mappings.add([modes.NORMAL], ["g]"],
1994             "Jump to the next off-screen element as defined by 'jumptags'",
1995             function (args) { buffer.findJump(args.arg, args.count, false, true); },
1996             { arg: true, count: true });
1997
1998         mappings.add([modes.NORMAL], ["]"],
1999             "Jump to the next element as defined by 'jumptags'",
2000             function (args) { buffer.findJump(args.arg, args.count, false); },
2001             { arg: true, count: true });
2002
2003         mappings.add([modes.NORMAL], ["{"],
2004             "Jump to the previous paragraph",
2005             function (args) { buffer.findJump("p", args.count, true); },
2006             { count: true });
2007
2008         mappings.add([modes.NORMAL], ["}"],
2009             "Jump to the next paragraph",
2010             function (args) { buffer.findJump("p", args.count, false); },
2011             { count: true });
2012
2013         mappings.add([modes.NORMAL], ["]]", "<next-page>"],
2014             "Follow the link labeled 'next' or '>' if it exists",
2015             function (args) {
2016                 buffer.findLink("next", options["nextpattern"], (args.count || 1) - 1, true);
2017             },
2018             { count: true });
2019
2020         mappings.add([modes.NORMAL], ["[[", "<previous-page>"],
2021             "Follow the link labeled 'prev', 'previous' or '<' if it exists",
2022             function (args) {
2023                 buffer.findLink("previous", options["previouspattern"], (args.count || 1) - 1, true);
2024             },
2025             { count: true });
2026
2027         mappings.add([modes.NORMAL], ["gf", "<view-source>"],
2028             "Toggle between rendered and source view",
2029             function () { buffer.viewSource(null, false); });
2030
2031         mappings.add([modes.NORMAL], ["gF", "<view-source-externally>"],
2032             "View source with an external editor",
2033             function () { buffer.viewSource(null, true); });
2034
2035         mappings.add([modes.NORMAL], ["gi", "<focus-input>"],
2036             "Focus last used input field",
2037             function (args) {
2038                 let elem = buffer.lastInputField;
2039
2040                 if (args.count >= 1 || !elem || !events.isContentNode(elem)) {
2041                     let xpath = ["frame", "iframe", "input", "xul:textbox", "textarea[not(@disabled) and not(@readonly)]"];
2042
2043                     let frames = buffer.allFrames(null, true);
2044
2045                     let elements = array.flatten(frames.map(function (win) [m for (m in DOM.XPath(xpath, win.document))]))
2046                                         .filter(function (elem) {
2047                         if (isinstance(elem, [Ci.nsIDOMHTMLFrameElement,
2048                                               Ci.nsIDOMHTMLIFrameElement]))
2049                             return Editor.getEditor(elem.contentWindow);
2050
2051                         elem = DOM(elem);
2052
2053                         if (elem[0].readOnly || !DOM(elem).isEditable)
2054                             return false;
2055
2056                         let style = elem.style;
2057                         let rect = elem.rect;
2058                         return elem.isVisible &&
2059                             (elem[0] instanceof Ci.nsIDOMXULTextBoxElement || style.MozUserFocus != "ignore") &&
2060                             rect.width && rect.height;
2061                     });
2062
2063                     dactyl.assert(elements.length > 0);
2064                     elem = elements[Math.constrain(args.count, 1, elements.length) - 1];
2065                 }
2066                 buffer.focusElement(elem);
2067                 DOM(elem).scrollIntoView();
2068             },
2069             { count: true });
2070
2071         function url() {
2072             let url = dactyl.clipboardRead();
2073             dactyl.assert(url, _("error.clipboardEmpty"));
2074
2075             let proto = /^([-\w]+):/.exec(url);
2076             if (proto && services.PROTOCOL + proto[1] in Cc && !RegExp(options["urlseparator"]).test(url))
2077                 return url.replace(/\s+/g, "");
2078             return url;
2079         }
2080
2081         mappings.add([modes.NORMAL], ["gP"],
2082             "Open (put) a URL based on the current clipboard contents in a new background buffer",
2083             function () {
2084                 dactyl.open(url(), { from: "paste", where: dactyl.NEW_TAB, background: true });
2085             });
2086
2087         mappings.add([modes.NORMAL], ["p", "<MiddleMouse>", "<open-clipboard-url>"],
2088             "Open (put) a URL based on the current clipboard contents in the current buffer",
2089             function () {
2090                 dactyl.open(url());
2091             });
2092
2093         mappings.add([modes.NORMAL], ["P", "<tab-open-clipboard-url>"],
2094             "Open (put) a URL based on the current clipboard contents in a new buffer",
2095             function () {
2096                 dactyl.open(url(), { from: "paste", where: dactyl.NEW_TAB });
2097             });
2098
2099         // reloading
2100         mappings.add([modes.NORMAL], ["r", "<reload>"],
2101             "Reload the current web page",
2102             function () { tabs.reload(tabs.getTab(), false); });
2103
2104         mappings.add([modes.NORMAL], ["R", "<full-reload>"],
2105             "Reload while skipping the cache",
2106             function () { tabs.reload(tabs.getTab(), true); });
2107
2108         // yanking
2109         mappings.add([modes.NORMAL], ["Y", "<yank-selection>"],
2110             "Copy selected text or current word",
2111             function () {
2112                 let sel = buffer.currentWord;
2113                 dactyl.assert(sel);
2114                 editor.setRegister(null, sel, true);
2115             });
2116
2117         // zooming
2118         mappings.add([modes.NORMAL], ["zi", "+", "<text-zoom-in>"],
2119             "Enlarge text zoom of current web page",
2120             function (args) { buffer.zoomIn(Math.max(args.count, 1), false); },
2121             { count: true });
2122
2123         mappings.add([modes.NORMAL], ["zm", "<text-zoom-more>"],
2124             "Enlarge text zoom of current web page by a larger amount",
2125             function (args) { buffer.zoomIn(Math.max(args.count, 1) * 3, false); },
2126             { count: true });
2127
2128         mappings.add([modes.NORMAL], ["zo", "-", "<text-zoom-out>"],
2129             "Reduce text zoom of current web page",
2130             function (args) { buffer.zoomOut(Math.max(args.count, 1), false); },
2131             { count: true });
2132
2133         mappings.add([modes.NORMAL], ["zr", "<text-zoom-reduce>"],
2134             "Reduce text zoom of current web page by a larger amount",
2135             function (args) { buffer.zoomOut(Math.max(args.count, 1) * 3, false); },
2136             { count: true });
2137
2138         mappings.add([modes.NORMAL], ["zz", "<text-zoom>"],
2139             "Set text zoom value of current web page",
2140             function (args) { buffer.setZoom(args.count > 1 ? args.count : 100, false); },
2141             { count: true });
2142
2143         mappings.add([modes.NORMAL], ["ZI", "zI", "<full-zoom-in>"],
2144             "Enlarge full zoom of current web page",
2145             function (args) { buffer.zoomIn(Math.max(args.count, 1), true); },
2146             { count: true });
2147
2148         mappings.add([modes.NORMAL], ["ZM", "zM", "<full-zoom-more>"],
2149             "Enlarge full zoom of current web page by a larger amount",
2150             function (args) { buffer.zoomIn(Math.max(args.count, 1) * 3, true); },
2151             { count: true });
2152
2153         mappings.add([modes.NORMAL], ["ZO", "zO", "<full-zoom-out>"],
2154             "Reduce full zoom of current web page",
2155             function (args) { buffer.zoomOut(Math.max(args.count, 1), true); },
2156             { count: true });
2157
2158         mappings.add([modes.NORMAL], ["ZR", "zR", "<full-zoom-reduce>"],
2159             "Reduce full zoom of current web page by a larger amount",
2160             function (args) { buffer.zoomOut(Math.max(args.count, 1) * 3, true); },
2161             { count: true });
2162
2163         mappings.add([modes.NORMAL], ["zZ", "<full-zoom>"],
2164             "Set full zoom value of current web page",
2165             function (args) { buffer.setZoom(args.count > 1 ? args.count : 100, true); },
2166             { count: true });
2167
2168         // page info
2169         mappings.add([modes.NORMAL], ["<C-g>", "<page-info>"],
2170             "Print the current file name",
2171             function () { buffer.showPageInfo(false); });
2172
2173         mappings.add([modes.NORMAL], ["g<C-g>", "<more-page-info>"],
2174             "Print file information",
2175             function () { buffer.showPageInfo(true); });
2176     },
2177     options: function initOptions(dactyl, modules, window) {
2178         let { Option, buffer, completion, config, options } = modules;
2179
2180         options.add(["encoding", "enc"],
2181             "The current buffer's character encoding",
2182             "string", "UTF-8",
2183             {
2184                 scope: Option.SCOPE_LOCAL,
2185                 getter: function () buffer.docShell.QueryInterface(Ci.nsIDocCharset).charset,
2186                 setter: function (val) {
2187                     if (options["encoding"] == val)
2188                         return val;
2189
2190                     // Stolen from browser.jar/content/browser/browser.js, more or less.
2191                     try {
2192                         buffer.docShell.QueryInterface(Ci.nsIDocCharset).charset = val;
2193                         window.PlacesUtils.history.setCharsetForURI(buffer.uri, val);
2194                         buffer.docShell.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
2195                     }
2196                     catch (e) { dactyl.reportError(e); }
2197                     return null;
2198                 },
2199                 completer: function (context) completion.charset(context)
2200             });
2201
2202         options.add(["iskeyword", "isk"],
2203             "Regular expression defining which characters constitute words",
2204             "string", '[^\\s.,!?:;/"\'^$%&?()[\\]{}<>#*+|=~_-]',
2205             {
2206                 setter: function (value) {
2207                     this.regexp = util.regexp(value);
2208                     return value;
2209                 },
2210                 validator: function (value) RegExp(value)
2211             });
2212
2213         options.add(["jumptags", "jt"],
2214             "XPath or CSS selector strings of jumpable elements for extended hint modes",
2215             "stringmap", {
2216                 "p": "p,table,ul,ol,blockquote",
2217                 "h": "h1,h2,h3,h4,h5,h6"
2218             },
2219             {
2220                 keepQuotes: true,
2221                 setter: function (vals) {
2222                     for (let [k, v] in Iterator(vals))
2223                         vals[k] = update(new String(v), { matcher: DOM.compileMatcher(Option.splitList(v)) });
2224                     return vals;
2225                 },
2226                 validator: function (value) DOM.validateMatcher.call(this, value)
2227                     && Object.keys(value).every(function (v) v.length == 1)
2228             });
2229
2230         options.add(["linenumbers", "ln"],
2231             "Patterns used to determine line numbers used by G",
2232             "sitemap", {
2233                 "code.google.com": '#nums [id^="nums_table"] a[href^="#"]',
2234                 "github.com": '.line_numbers>*',
2235                 "mxr.mozilla.org": 'a.l',
2236                 "pastebin.com": '#code_frame>div>ol>li',
2237                 "addons.mozilla.org": '.gutter>.line>a',
2238                 "*": '/* Hgweb/Gitweb */ .completecodeline a.codeline, a.linenr'
2239             },
2240             {
2241                 getLine: function getLine(doc, line) {
2242                     let uri = util.newURI(doc.documentURI);
2243                     for (let filter in values(this.value))
2244                         if (filter(uri, doc)) {
2245                             if (/^func:/.test(filter.result))
2246                                 var res = dactyl.userEval("(" + Option.dequote(filter.result.substr(5)) + ")")(doc, line);
2247                             else
2248                                 res = iter.nth(filter.matcher(doc),
2249                                                function (elem) (elem.nodeValue || elem.textContent).trim() == line && DOM(elem).display != "none",
2250                                                0)
2251                                    || iter.nth(filter.matcher(doc), util.identity, line - 1);
2252                             if (res)
2253                                 break;
2254                         }
2255
2256                     return res;
2257                 },
2258
2259                 keepQuotes: true,
2260
2261                 setter: function (vals) {
2262                     for (let value in values(vals))
2263                         if (!/^func:/.test(value.result))
2264                             value.matcher = DOM.compileMatcher(Option.splitList(value.result));
2265                     return vals;
2266                 },
2267
2268                 validator: function validate(values) {
2269                     return this.testValues(values, function (value) {
2270                         if (/^func:/.test(value))
2271                             return callable(dactyl.userEval("(" + Option.dequote(value.substr(5)) + ")"));
2272                         else
2273                             return DOM.testMatcher(Option.dequote(value));
2274                     });
2275                 }
2276             });
2277
2278         options.add(["nextpattern"],
2279             "Patterns to use when guessing the next page in a document sequence",
2280             "regexplist", UTF8(/'\bnext\b',^>$,^(>>|»)$,^(>|»),(>|»)$,'\bmore\b'/.source),
2281             { regexpFlags: "i" });
2282
2283         options.add(["previouspattern"],
2284             "Patterns to use when guessing the previous page in a document sequence",
2285             "regexplist", UTF8(/'\bprev|previous\b',^<$,^(<<|«)$,^(<|«),(<|«)$/.source),
2286             { regexpFlags: "i" });
2287
2288         options.add(["pageinfo", "pa"],
2289             "Define which sections are shown by the :pageinfo command",
2290             "charlist", "gesfm",
2291             { get values() values(Buffer.pageInfo).toObject() });
2292
2293         options.add(["scroll", "scr"],
2294             "Number of lines to scroll with <C-u> and <C-d> commands",
2295             "number", 0,
2296             { validator: function (value) value >= 0 });
2297
2298         options.add(["showstatuslinks", "ssli"],
2299             "Where to show the destination of the link under the cursor",
2300             "string", "status",
2301             {
2302                 values: {
2303                     "": "Don't show link destinations",
2304                     "status": "Show link destinations in the status line",
2305                     "command": "Show link destinations in the command line"
2306                 }
2307             });
2308
2309         options.add(["scrolltime", "sct"],
2310             "The time, in milliseconds, in which to smooth scroll to a new position",
2311             "number", 100);
2312
2313         options.add(["scrollsteps", "scs"],
2314             "The number of steps in which to smooth scroll to a new position",
2315             "number", 5,
2316             {
2317                 PREF: "general.smoothScroll",
2318
2319                 initValue: function () {},
2320
2321                 getter: function getter(value) !prefs.get(this.PREF) ? 1 : value,
2322
2323                 setter: function setter(value) {
2324                     prefs.set(this.PREF, value > 1);
2325                     if (value > 1)
2326                         return value;
2327                 },
2328
2329                 validator: function (value) value > 0
2330             });
2331
2332         options.add(["usermode", "um"],
2333             "Show current website without styling defined by the author",
2334             "boolean", false,
2335             {
2336                 setter: function (value) buffer.contentViewer.authorStyleDisabled = value,
2337                 getter: function () buffer.contentViewer.authorStyleDisabled
2338             });
2339
2340         options.add(["yankshort", "ys"],
2341             "Yank the canonical short URL of a web page where provided",
2342             "sitelist", ["youtube.com", "bugzilla.mozilla.org"]);
2343     }
2344 });
2345
2346 Buffer.addPageInfoSection("e", "Search Engines", function (verbose) {
2347     let n = 1;
2348     let nEngines = 0;
2349
2350     for (let { document: doc } in values(this.allFrames())) {
2351         let engines = DOM("link[href][rel=search][type='application/opensearchdescription+xml']", doc);
2352         nEngines += engines.length;
2353
2354         if (verbose)
2355             for (let link in engines)
2356                 yield [link.title || /*L*/ "Engine " + n++,
2357                        <a xmlns={XHTML} href={link.href}
2358                           onclick="if (event.button == 0) { window.external.AddSearchProvider(this.href); return false; }"
2359                           highlight="URL">{link.href}</a>];
2360     }
2361
2362     if (!verbose && nEngines)
2363         yield nEngines + /*L*/" engine" + (nEngines > 1 ? "s" : "");
2364 });
2365
2366 Buffer.addPageInfoSection("f", "Feeds", function (verbose) {
2367     const feedTypes = {
2368         "application/rss+xml": "RSS",
2369         "application/atom+xml": "Atom",
2370         "text/xml": "XML",
2371         "application/xml": "XML",
2372         "application/rdf+xml": "XML"
2373     };
2374
2375     function isValidFeed(data, principal, isFeed) {
2376         if (!data || !principal)
2377             return false;
2378
2379         if (!isFeed) {
2380             var type = data.type && data.type.toLowerCase();
2381             type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
2382
2383             isFeed = ["application/rss+xml", "application/atom+xml"].indexOf(type) >= 0 ||
2384                      // really slimy: general XML types with magic letters in the title
2385                      type in feedTypes && /\brss\b/i.test(data.title);
2386         }
2387
2388         if (isFeed) {
2389             try {
2390                 services.security.checkLoadURIStrWithPrincipal(principal, data.href,
2391                         services.security.DISALLOW_INHERIT_PRINCIPAL);
2392             }
2393             catch (e) {
2394                 isFeed = false;
2395             }
2396         }
2397
2398         if (type)
2399             data.type = type;
2400
2401         return isFeed;
2402     }
2403
2404     let nFeed = 0;
2405     for (let [i, win] in Iterator(this.allFrames())) {
2406         let doc = win.document;
2407
2408         for (let link in DOM("link[href][rel=feed], link[href][rel=alternate][type]", doc)) {
2409             let rel = link.rel.toLowerCase();
2410             let feed = { title: link.title, href: link.href, type: link.type || "" };
2411             if (isValidFeed(feed, doc.nodePrincipal, rel == "feed")) {
2412                 nFeed++;
2413                 let type = feedTypes[feed.type] || "RSS";
2414                 if (verbose)
2415                     yield [feed.title, template.highlightURL(feed.href, true) + <span class="extra-info">&#xa0;({type})</span>];
2416             }
2417         }
2418
2419     }
2420
2421     if (!verbose && nFeed)
2422         yield nFeed + /*L*/" feed" + (nFeed > 1 ? "s" : "");
2423 });
2424
2425 Buffer.addPageInfoSection("g", "General Info", function (verbose) {
2426     let doc = this.focusedFrame.document;
2427
2428     // get file size
2429     const ACCESS_READ = Ci.nsICache.ACCESS_READ;
2430     let cacheKey = doc.documentURI;
2431
2432     for (let proto in array.iterValues(["HTTP", "FTP"])) {
2433         try {
2434             var cacheEntryDescriptor = services.cache.createSession(proto, 0, true)
2435                                                .openCacheEntry(cacheKey, ACCESS_READ, false);
2436             break;
2437         }
2438         catch (e) {}
2439     }
2440
2441     let pageSize = []; // [0] bytes; [1] kbytes
2442     if (cacheEntryDescriptor) {
2443         pageSize[0] = util.formatBytes(cacheEntryDescriptor.dataSize, 0, false);
2444         pageSize[1] = util.formatBytes(cacheEntryDescriptor.dataSize, 2, true);
2445         if (pageSize[1] == pageSize[0])
2446             pageSize.length = 1; // don't output "xx Bytes" twice
2447     }
2448
2449     let lastModVerbose = new Date(doc.lastModified).toLocaleString();
2450     let lastMod = new Date(doc.lastModified).toLocaleFormat("%x %X");
2451
2452     if (lastModVerbose == "Invalid Date" || new Date(doc.lastModified).getFullYear() == 1970)
2453         lastModVerbose = lastMod = null;
2454
2455     if (!verbose) {
2456         if (pageSize[0])
2457             yield (pageSize[1] || pageSize[0]) + /*L*/" bytes";
2458         yield lastMod;
2459         return;
2460     }
2461
2462     yield ["Title", doc.title];
2463     yield ["URL", template.highlightURL(doc.location.href, true)];
2464
2465     let ref = "referrer" in doc && doc.referrer;
2466     if (ref)
2467         yield ["Referrer", template.highlightURL(ref, true)];
2468
2469     if (pageSize[0])
2470         yield ["File Size", pageSize[1] ? pageSize[1] + " (" + pageSize[0] + ")"
2471                                         : pageSize[0]];
2472
2473     yield ["Mime-Type", doc.contentType];
2474     yield ["Encoding", doc.characterSet];
2475     yield ["Compatibility", doc.compatMode == "BackCompat" ? "Quirks Mode" : "Full/Almost Standards Mode"];
2476     if (lastModVerbose)
2477         yield ["Last Modified", lastModVerbose];
2478 });
2479
2480 Buffer.addPageInfoSection("m", "Meta Tags", function (verbose) {
2481     if (!verbose)
2482         return [];
2483
2484     // get meta tag data, sort and put into pageMeta[]
2485     let metaNodes = this.focusedFrame.document.getElementsByTagName("meta");
2486
2487     return Array.map(metaNodes, function (node) [(node.name || node.httpEquiv), template.highlightURL(node.content)])
2488                 .sort(function (a, b) util.compareIgnoreCase(a[0], b[0]));
2489 });
2490
2491 Buffer.addPageInfoSection("s", "Security", function (verbose) {
2492     let { statusline } = this.modules
2493
2494     let identity = this.topWindow.gIdentityHandler;
2495
2496     if (!verbose || !identity)
2497         return; // For now
2498
2499     // Modified from Firefox
2500     function location(data) array.compact([
2501         data.city, data.state, data.country
2502     ]).join(", ");
2503
2504     switch (statusline.security) {
2505     case "secure":
2506     case "extended":
2507         var data = identity.getIdentityData();
2508
2509         yield ["Host", identity.getEffectiveHost()];
2510
2511         if (statusline.security === "extended")
2512             yield ["Owner", data.subjectOrg];
2513         else
2514             yield ["Owner", _("pageinfo.s.ownerUnverified", data.subjectOrg)];
2515
2516         if (location(data).length)
2517             yield ["Location", location(data)];
2518
2519         yield ["Verified by", data.caOrg];
2520
2521         if (identity._overrideService.hasMatchingOverride(identity._lastLocation.hostname,
2522                                                       (identity._lastLocation.port || 443),
2523                                                       data.cert, {}, {}))
2524             yield ["User exception", /*L*/"true"];
2525         break;
2526     }
2527 });
2528
2529 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
2530
2531 endModule();
2532
2533 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: