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