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