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