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