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>
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
9 Components.utils.import("resource://dactyl/bootstrap.jsm");
10 defineModule("buffer", {
11 exports: ["Buffer", "buffer"],
12 require: ["prefs", "services", "util"]
15 this.lazyRequire("finder", ["RangeFind"]);
16 this.lazyRequire("io", ["io"]);
17 this.lazyRequire("overlay", ["overlay"]);
18 this.lazyRequire("storage", ["File", "storage"]);
19 this.lazyRequire("template", ["template"]);
22 * A class to manage the primary web content buffer. The name comes
23 * from Vim's term, 'buffer', which signifies instances of open
27 var Buffer = Module("Buffer", {
28 Local: function Local(dactyl, modules, window) ({
30 return window.content;
32 let win = services.focus.focusedWindow;
33 if (!win || win == window || util.topWindow(win) != window)
35 if (win.top == window)
41 init: function init(win) {
46 get addPageInfoSection() Buffer.closure.addPageInfoSection,
48 get pageInfo() Buffer.pageInfo,
50 // called when the active document is scrolled
51 _updateBufferPosition: function _updateBufferPosition() {
52 this.modules.statusline.updateBufferPosition();
53 this.modules.commandline.clear(true);
57 * @property {Array} The alternative style sheets for the current
58 * buffer. Only returns style sheets for the 'screen' media type.
60 get alternateStyleSheets() {
61 let stylesheets = array.flatten(
62 this.allFrames().map(function (w) Array.slice(w.document.styleSheets)));
64 return stylesheets.filter(
65 function (stylesheet) /^(screen|all|)$/i.test(stylesheet.media.mediaText) && !/^\s*$/.test(stylesheet.title)
69 climbUrlPath: function climbUrlPath(count) {
70 let { dactyl } = this.modules;
72 let url = this.documentURI.clone();
73 dactyl.assert(url instanceof Ci.nsIURL);
75 while (count-- && url.path != "/")
76 url.path = url.path.replace(/[^\/]*\/*$/, "");
78 dactyl.assert(!url.equals(this.documentURI));
79 dactyl.open(url.spec);
82 incrementURL: function incrementURL(count) {
83 let { dactyl } = this.modules;
85 let matches = this.uri.spec.match(/(.*?)(\d+)(\D*)$/);
86 dactyl.assert(matches);
87 let oldNum = matches[2];
89 // disallow negative numbers as trailing numbers are often proceeded by hyphens
90 let newNum = String(Math.max(parseInt(oldNum, 10) + count, 0));
91 if (/^0/.test(oldNum))
92 while (newNum.length < oldNum.length)
93 newNum = "0" + newNum;
96 dactyl.open(matches.slice(1).join(""));
100 * @property {number} True when the buffer is fully loaded.
102 get loaded() Math.min.apply(null,
104 .map(function (frame) ["loading", "interactive", "complete"]
105 .indexOf(frame.document.readyState))),
108 * @property {Object} The local state store for the currently selected
114 let store = overlay.getData(doc, "buffer", null);
115 if (!store || !this.localStorePrototype.isPrototypeOf(store))
116 store = overlay.setData(doc, "buffer", Object.create(this.localStorePrototype));
117 return store.instance = store;
120 localStorePrototype: memoize({
127 * @property {Node} The last focused input field in the buffer. Used
128 * by the "gi" key binding.
130 get lastInputField() {
131 let field = this.localStore.lastInputField && this.localStore.lastInputField.get();
133 let doc = field && field.ownerDocument;
134 let win = doc && doc.defaultView;
135 return win && doc === win.document ? field : null;
137 set lastInputField(value) { this.localStore.lastInputField = util.weakReference(value); },
140 * @property {nsIURI} The current top-level document.
142 get doc() this.win.document,
144 get docShell() util.docShell(this.win),
146 get modules() this.topWindow.dactyl.modules,
149 topWindow: Class.Memoize(function () util.topWindow(this.win)),
152 * @property {nsIURI} The current top-level document's URI.
154 get uri() util.newURI(this.win.location.href),
157 * @property {nsIURI} The current top-level document's URI, sans any
158 * fragment identifier.
160 get documentURI() this.doc.documentURIObject || util.newURI(this.doc.documentURI),
163 * @property {string} The current top-level document's URL.
165 get URL() update(new String(this.win.location.href), util.newURI(this.win.location.href)),
168 * @property {number} The buffer's height in pixels.
170 get pageHeight() this.win.innerHeight,
172 get contentViewer() this.docShell.contentViewer
173 .QueryInterface(Components.interfaces.nsIMarkupDocumentViewer),
176 * @property {number} The current browser's zoom level, as a
177 * percentage with 100 as 'normal'.
180 let v = this.contentViewer;
181 return v[v.textZoom == 1 ? "fullZoom" : "textZoom"] * 100
183 set zoomLevel(value) { this.setZoom(value, this.fullZoom); },
186 * @property {boolean} Whether the current browser is using full
187 * zoom, as opposed to text zoom.
189 get fullZoom() this.ZoomManager.useFullZoom,
190 set fullZoom(value) { this.setZoom(this.zoomLevel, value); },
192 get ZoomManager() this.topWindow.ZoomManager,
195 * @property {string} The current document's title.
197 get title() this.doc.title,
200 * @property {number} The buffer's horizontal scroll percentile.
202 get scrollXPercent() {
203 let elem = Buffer.Scrollable(this.findScrollable(0, true));
204 if (elem.scrollWidth - elem.clientWidth === 0)
206 return elem.scrollLeft * 100 / (elem.scrollWidth - elem.clientWidth);
210 * @property {number} The buffer's vertical scroll percentile.
212 get scrollYPercent() {
213 let elem = Buffer.Scrollable(this.findScrollable(0, false));
214 if (elem.scrollHeight - elem.clientHeight === 0)
216 return elem.scrollTop * 100 / (elem.scrollHeight - elem.clientHeight);
220 * @property {{ x: number, y: number }} The buffer's current scroll position
221 * as reported by {@link Buffer.getScrollPosition}.
223 get scrollPosition() Buffer.getScrollPosition(this.findScrollable(0, false)),
226 * Returns a list of all frames in the given window or current buffer.
228 allFrames: function allFrames(win, focusedFirst) {
230 (function rec(frame) {
231 if (true || frame.document.body instanceof Ci.nsIDOMHTMLBodyElement)
233 Array.forEach(frame.frames, rec);
237 return frames.filter(function (f) f === this.focusedFrame).concat(
238 frames.filter(function (f) f !== this.focusedFrame));
243 * @property {Window} Returns the currently focused frame.
246 let frame = this.localStore.focusedFrame;
247 return frame && frame.get() || this.win;
249 set focusedFrame(frame) {
250 this.localStore.focusedFrame = util.weakReference(frame);
254 * Returns the currently selected word. If the selection is
255 * null, it tries to guess the word that the caret is
260 get currentWord() Buffer.currentWord(this.focusedFrame),
261 getCurrentWord: deprecated("buffer.currentWord", function getCurrentWord() Buffer.currentWord(this.focusedFrame, true)),
264 * Returns true if a scripts are allowed to focus the given input
265 * element or input elements in the given window.
267 * @param {Node|Window}
270 focusAllowed: function focusAllowed(elem) {
271 if (elem instanceof Ci.nsIDOMWindow && !DOM(elem).isEditable)
274 let { options } = this.modules;
276 let doc = elem.ownerDocument || elem.document || elem;
277 switch (options.get("strictfocus").getKey(doc.documentURIObject || util.newURI(doc.documentURI), "moderate")) {
279 return overlay.getData(elem)["focus-allowed"]
280 || elem.frameElement && overlay.getData(elem.frameElement)["focus-allowed"];
282 return overlay.getData(doc, "focus-allowed")
283 || elem.frameElement && overlay.getData(elem.frameElement.ownerDocument)["focus-allowed"];
290 * Focuses the given element. In contrast to a simple
291 * elem.focus() call, this function works for iframes and
294 * @param {Node} elem The element to focus.
296 focusElement: function focusElement(elem) {
297 let win = elem.ownerDocument && elem.ownerDocument.defaultView || elem;
298 overlay.setData(elem, "focus-allowed", true);
299 overlay.setData(win.document, "focus-allowed", true);
301 if (isinstance(elem, [Ci.nsIDOMHTMLFrameElement,
302 Ci.nsIDOMHTMLIFrameElement]))
303 elem = elem.contentWindow;
306 overlay.setData(elem.document, "focus-allowed", true);
308 if (elem instanceof Ci.nsIDOMHTMLInputElement && elem.type == "file") {
309 Buffer.openUploadPrompt(elem);
310 this.lastInputField = elem;
313 if (isinstance(elem, [Ci.nsIDOMHTMLInputElement,
314 Ci.nsIDOMXULTextBoxElement]))
315 var flags = services.focus.FLAG_BYMOUSE;
317 flags = services.focus.FLAG_SHOWRING;
319 // Hack to deal with current versions of Firefox misplacing
321 if (!overlay.getData(elem, "had-focus", false) && elem.value &&
322 elem instanceof Ci.nsIDOMHTMLInputElement &&
323 DOM(elem).isEditable &&
324 elem.selectionStart != null &&
325 elem.selectionStart == elem.selectionEnd)
326 elem.selectionStart = elem.selectionEnd = elem.value.length;
328 DOM(elem).focus(flags);
330 if (elem instanceof Ci.nsIDOMWindow) {
331 let sel = elem.getSelection();
332 if (sel && !sel.rangeCount)
333 sel.addRange(RangeFind.endpoint(
334 RangeFind.nodeRange(elem.document.body || elem.document.documentElement),
338 let range = RangeFind.nodeRange(elem);
339 let sel = (elem.ownerDocument || elem).defaultView.getSelection();
340 if (!sel.rangeCount || !RangeFind.intersects(range, sel.getRangeAt(0))) {
341 range.collapse(true);
342 sel.removeAllRanges();
348 if (elem instanceof Ci.nsIDOMHTMLAreaElement) {
350 let [x, y] = elem.getAttribute("coords").split(",").map(parseFloat);
352 DOM(elem).mouseover({ screenX: x, screenY: y });
360 * Find the *count*th last link on a page matching one of the given
361 * regular expressions, or with a @rel or @rev attribute matching
362 * the given relation. Each frame is searched beginning with the
363 * last link and progressing to the first, once checking for
364 * matching @rel or @rev properties, and then once for each given
365 * regular expression. The first match is returned. All frames of
366 * the page are searched, beginning with the currently focused.
368 * If follow is true, the link is followed.
370 * @param {string} rel The relationship to look for.
371 * @param {[RegExp]} regexps The regular expressions to search for.
372 * @param {number} count The nth matching link to follow.
373 * @param {bool} follow Whether to follow the matching link.
374 * @param {string} path The CSS to use for the search. @optional
376 findLink: function findLink(rel, regexps, count, follow, path) {
377 let { Hints, dactyl, options } = this.modules;
379 let selector = path || options.get("hinttags").stringDefaultValue;
381 function followFrame(frame) {
382 function iter(elems) {
383 for (let i = 0; i < elems.length; i++)
384 if (elems[i].rel.toLowerCase() === rel || elems[i].rev.toLowerCase() === rel)
388 let elems = frame.document.getElementsByTagName("link");
389 for (let elem in iter(elems))
392 elems = frame.document.getElementsByTagName("a");
393 for (let elem in iter(elems))
396 function a(regexp, elem) regexp.test(elem.textContent) === regexp.result ||
397 Array.some(elem.childNodes, function (child) regexp.test(child.alt) === regexp.result);
398 function b(regexp, elem) regexp.test(elem.title) === regexp.result;
400 let res = Array.filter(frame.document.querySelectorAll(selector), Hints.isVisible);
401 for (let test in values([a, b]))
402 for (let regexp in values(regexps))
403 for (let i in util.range(res.length, 0, -1))
404 if (test(regexp, res[i]))
408 for (let frame in values(this.allFrames(null, true)))
409 for (let elem in followFrame(frame))
412 this.followLink(elem, dactyl.CURRENT_TAB);
419 followDocumentRelationship: deprecated("buffer.findLink",
420 function followDocumentRelationship(rel) {
421 let { options } = this.modules;
423 this.findLink(rel, options[rel + "pattern"], 0, true);
427 * Fakes a click on a link.
429 * @param {Node} elem The element to click.
430 * @param {number} where Where to open the link. See
431 * {@link dactyl.open}.
433 followLink: function followLink(elem, where) {
434 let { dactyl } = this.modules;
436 let doc = elem.ownerDocument;
437 let win = doc.defaultView;
438 let { left: offsetX, top: offsetY } = elem.getBoundingClientRect();
440 if (isinstance(elem, [Ci.nsIDOMHTMLFrameElement,
441 Ci.nsIDOMHTMLIFrameElement]))
442 return this.focusElement(elem);
444 if (isinstance(elem, Ci.nsIDOMHTMLLinkElement))
445 return dactyl.open(elem.href, where);
447 if (elem instanceof Ci.nsIDOMHTMLAreaElement) { // for imagemap
448 let coords = elem.getAttribute("coords").split(",");
449 offsetX = Number(coords[0]) + 1;
450 offsetY = Number(coords[1]) + 1;
452 else if (elem instanceof Ci.nsIDOMHTMLInputElement && elem.type == "file") {
453 Buffer.openUploadPrompt(elem);
457 let { dactyl } = this.modules;
459 let ctrlKey = false, shiftKey = false;
461 switch (dactyl.forceTarget || where) {
463 case dactyl.NEW_BACKGROUND_TAB:
465 shiftKey = dactyl.forceBackground != null ? dactyl.forceBackground
466 : where != dactyl.NEW_BACKGROUND_TAB;
468 case dactyl.NEW_WINDOW:
471 case dactyl.CURRENT_TAB:
475 this.focusElement(elem);
477 prefs.withContext(function () {
478 prefs.set("browser.tabs.loadInBackground", true);
480 button: button, screenX: offsetX, screenY: offsetY,
481 ctrlKey: ctrlKey, shiftKey: shiftKey, metaKey: ctrlKey
484 DOM(elem).mousedown(params).mouseup(params);
485 if (!config.haveGecko("2b"))
486 DOM(elem).click(params);
488 let sel = util.selectionController(win);
489 sel.getSelection(sel.SELECTION_FOCUS_REGION).collapseToStart();
494 * Resets the caret position so that it resides within the current
497 resetCaret: function resetCaret() {
498 function visible(range) util.intersection(DOM(range).rect, viewport);
500 function getRanges(rect) {
501 let nodes = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils)
502 .nodesFromRect(rect.x, rect.y, 0, rect.width, rect.height, 0, false, false);
503 return Array.filter(nodes, function (n) n instanceof Ci.nsIDOMText)
504 .map(RangeFind.nodeContents);
507 let win = this.focusedFrame;
508 let doc = win.document;
509 let sel = win.getSelection();
510 let { viewport } = DOM(win);
512 if (sel.rangeCount) {
513 var range = sel.getRangeAt(0);
514 if (visible(range).height > 0)
517 var { rect } = DOM(range);
518 var reverse = rect.bottom > viewport.bottom;
520 rect = { x: rect.left, y: 0, width: rect.width, height: win.innerHeight };
523 let w = win.innerWidth;
524 rect = { x: w / 3, y: 0, width: w / 3, height: win.innerHeight };
527 var reduce = function (a, b) DOM(a).rect.top < DOM(b).rect.top ? a : b;
531 reduce = function (a, b) DOM(b).rect.bottom > DOM(a).rect.bottom ? b : a;
533 y = win.innerHeight - 1;
536 let ranges = getRanges(rect);
538 ranges = getRanges({ x: 0, y: y, width: win.innerWidth, height: 0 });
541 range = ranges.reduce(reduce);
544 range.collapse(!reverse);
545 sel.removeAllRanges();
548 if (visible(range).height > 0)
551 var { startContainer, startOffset } = range;
552 sel.modify("move", dir, "line");
553 range = sel.getRangeAt(0);
555 while (startContainer != range.startContainer || startOffset != range.startOffset);
557 sel.modify("move", reverse ? "forward" : "backward", "lineboundary");
562 sel.collapse(doc.body || doc.querySelector("body") || doc.documentElement,
567 * @property {nsISelection} The current document's normal selection.
569 get selection() this.win.getSelection(),
572 * @property {nsISelectionController} The current document's selection
575 get selectionController() util.selectionController(this.focusedFrame),
578 * Opens the appropriate context menu for *elem*.
580 * @param {Node} elem The context element.
582 openContextMenu: deprecated("DOM#contextmenu", function openContextMenu(elem) DOM(elem).contextmenu()),
585 * Saves a page link to disk.
587 * @param {HTMLAnchorElement} elem The page link to save.
589 saveLink: function saveLink(elem) {
590 let { completion, dactyl, io } = this.modules;
593 let doc = elem.ownerDocument;
594 let uri = util.newURI(elem.href || elem.src, null, util.newURI(elem.baseURI));
595 let referrer = util.newURI(doc.documentURI, doc.characterSet);
598 services.security.checkLoadURIWithPrincipal(doc.nodePrincipal, uri,
599 services.security.STANDARD);
601 io.CommandFileMode(_("buffer.prompt.saveLink") + " ", {
602 onSubmit: function (path) {
603 let file = io.File(path);
604 if (file.exists() && file.isDirectory())
605 file.append(Buffer.getDefaultNames(elem)[0][0]);
609 file.create(File.NORMAL_FILE_TYPE, octal(644));
612 util.assert(false, _("save.invalidDestination", e.name));
615 self.saveURI(uri, file);
618 completer: function (context) completion.savePage(context, elem)
627 * Saves the contents of a URI to disk.
629 * @param {nsIURI} uri The URI to save
630 * @param {nsIFile} file The file into which to write the result.
632 saveURI: function saveURI(uri, file, callback, self) {
633 var persist = services.Persist();
634 persist.persistFlags = persist.PERSIST_FLAGS_FROM_CACHE
635 | persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
637 let window = this.topWindow;
639 file.create(Ci.nsIFile.NORMAL_FILE_TYPE, octal(666));
641 let downloadListener = new window.DownloadListener(window,
642 services.Transfer(uri, File(file).URI, "",
643 null, null, null, persist));
646 persist.progressListener = update(Object.create(downloadListener), {
647 onStateChange: util.wrapCallback(function onStateChange(progress, request, flags, status) {
648 if (callback && (flags & Ci.nsIWebProgressListener.STATE_STOP) && status == 0)
649 util.trapErrors(callback, self, uri, file, progress, request, flags, status);
651 return onStateChange.superapply(this, arguments);
655 persist.progressListener = downloadListener;
657 persist.saveURI(uri, null, null, null, null, file);
661 * Scrolls the currently active element horizontally. See
662 * {@link Buffer.scrollHorizontal} for parameters.
664 scrollHorizontal: function scrollHorizontal(increment, number)
665 Buffer.scrollHorizontal(this.findScrollable(number, true), increment, number),
668 * Scrolls the currently active element vertically. See
669 * {@link Buffer.scrollVertical} for parameters.
671 scrollVertical: function scrollVertical(increment, number)
672 Buffer.scrollVertical(this.findScrollable(number, false), increment, number),
675 * Scrolls the currently active element to the given horizontal and
676 * vertical percentages. See {@link Buffer.scrollToPercent} for
679 scrollToPercent: function scrollToPercent(horizontal, vertical)
680 Buffer.scrollToPercent(this.findScrollable(0, vertical == null), horizontal, vertical),
683 * Scrolls the currently active element to the given horizontal and
684 * vertical positions. See {@link Buffer.scrollToPosition} for
687 scrollToPosition: function scrollToPosition(horizontal, vertical)
688 Buffer.scrollToPosition(this.findScrollable(0, vertical == null), horizontal, vertical),
690 _scrollByScrollSize: function _scrollByScrollSize(count, direction) {
691 let { options } = this.modules;
694 options["scroll"] = count;
695 this.scrollByScrollSize(direction);
699 * Scrolls the buffer vertically 'scroll' lines.
701 * @param {boolean} direction The direction to scroll. If true then
702 * scroll up and if false scroll down.
703 * @param {number} count The multiple of 'scroll' lines to scroll.
706 scrollByScrollSize: function scrollByScrollSize(direction, count) {
707 let { options } = this.modules;
709 direction = direction ? 1 : -1;
712 if (options["scroll"] > 0)
713 this.scrollVertical("lines", options["scroll"] * direction);
715 this.scrollVertical("pages", direction / 2);
719 * Find the best candidate scrollable element for the given
720 * direction and orientation.
722 * @param {number} dir The direction in which the element must be
723 * able to scroll. Negative numbers represent up or left, while
724 * positive numbers represent down or right.
725 * @param {boolean} horizontal If true, look for horizontally
726 * scrollable elements, otherwise look for vertically scrollable
729 findScrollable: function findScrollable(dir, horizontal) {
730 function find(elem) {
731 while (elem && !(elem instanceof Ci.nsIDOMElement) && elem.parentNode)
732 elem = elem.parentNode;
733 for (; elem instanceof Ci.nsIDOMElement; elem = elem.parentNode)
734 if (Buffer.isScrollable(elem, dir, horizontal))
741 var elem = this.focusedFrame.document.activeElement;
742 if (elem == elem.ownerDocument.body)
748 var sel = this.focusedFrame.getSelection();
751 if (!elem && sel && sel.rangeCount)
752 elem = sel.getRangeAt(0).startContainer;
756 if (!(elem instanceof Ci.nsIDOMElement)) {
757 let doc = this.findScrollableWindow().document;
758 elem = find(doc.body || doc.getElementsByTagName("body")[0] ||
759 doc.documentElement);
761 let doc = this.focusedFrame.document;
762 return util.assert(elem || doc.body || doc.documentElement);
766 * Find the best candidate scrollable frame in the current buffer.
768 findScrollableWindow: function findScrollableWindow() {
769 let { document } = this.topWindow;
771 let win = document.commandDispatcher.focusedWindow;
772 if (win && (win.scrollMaxX > 0 || win.scrollMaxY > 0))
775 let win = this.focusedFrame;
776 if (win && (win.scrollMaxX > 0 || win.scrollMaxY > 0))
780 if (win.scrollMaxX > 0 || win.scrollMaxY > 0)
783 for (let frame in array.iterValues(win.frames))
784 if (frame.scrollMaxX > 0 || frame.scrollMaxY > 0)
791 * Finds the next visible element for the node path in 'jumptags'
794 * @param {string} arg The element in 'jumptags' to use for the search.
795 * @param {number} count The number of elements to jump.
797 * @param {boolean} reverse If true, search backwards. @optional
798 * @param {boolean} offScreen If true, include only off-screen elements. @optional
800 findJump: function findJump(arg, count, reverse, offScreen) {
801 let { marks, options } = this.modules;
807 let path = options["jumptags"][arg];
808 util.assert(path, _("error.invalidArgument", arg));
810 let distance = reverse ? function (rect) -rect.top : function (rect) rect.top;
811 let elems = [[e, distance(e.getBoundingClientRect())] for (e in path.matcher(this.focusedFrame.document))]
812 .filter(function (e) e[1] > FUDGE)
813 .sort(function (a, b) a[1] - b[1])
815 if (offScreen && !reverse)
816 elems = elems.filter(function (e) e[1] > this, this.topWindow.innerHeight);
818 let idx = Math.min((count || 1) - 1, elems.length);
819 util.assert(idx in elems);
821 let elem = elems[idx][0];
822 elem.scrollIntoView(true);
824 let sel = elem.ownerDocument.defaultView.getSelection();
825 sel.removeAllRanges();
826 sel.addRange(RangeFind.endpoint(RangeFind.nodeRange(elem), true));
829 // TODO: allow callback for filtering out unwanted frames? User defined?
831 * Shifts the focus to another frame within the buffer. Each buffer
832 * contains at least one frame.
834 * @param {number} count The number of frames to skip through. A negative
835 * count skips backwards.
837 shiftFrameFocus: function shiftFrameFocus(count) {
838 if (!(this.doc instanceof Ci.nsIDOMHTMLDocument))
841 let frames = this.allFrames();
843 if (frames.length == 0) // currently top is always included
846 // remove all hidden frames
847 frames = frames.filter(function (frame) !(frame.document.body instanceof Ci.nsIDOMHTMLFrameSetElement))
848 .filter(function (frame) !frame.frameElement ||
849 let (rect = frame.frameElement.getBoundingClientRect())
850 rect.width && rect.height);
852 // find the currently focused frame index
853 let current = Math.max(0, frames.indexOf(this.focusedFrame));
855 // calculate the next frame to focus
856 let next = current + count;
857 if (next < 0 || next >= frames.length)
859 next = Math.constrain(next, 0, frames.length - 1);
861 // focus next frame and scroll into view
862 DOM(frames[next]).focus();
863 if (frames[next] != this.win)
864 DOM(frames[next].frameElement).scrollIntoView();
866 // add the frame indicator
867 let doc = frames[next].document;
868 let indicator = DOM(<div highlight="FrameIndicator"/>, doc)
869 .appendTo(doc.body || doc.documentElement || doc);
871 util.timeout(function () { indicator.remove(); }, 500);
874 //doc.body.setAttributeNS(NS.uri, "activeframe", "true");
875 //util.timeout(function () { doc.body.removeAttributeNS(NS.uri, "activeframe"); }, 500);
878 // similar to pageInfo
879 // TODO: print more useful information, just like the DOM inspector
881 * Displays information about the specified element.
883 * @param {Node} elem The element to query.
885 showElementInfo: function showElementInfo(elem) {
886 let { dactyl } = this.modules;
888 XML.ignoreWhitespace = XML.prettyPrinting = false;
889 dactyl.echo(<><!--L-->Element:<br/>{util.objectToString(elem, true)}</>);
893 * Displays information about the current buffer.
895 * @param {boolean} verbose Display more verbose information.
896 * @param {string} sections A string limiting the displayed sections.
897 * @default The value of 'pageinfo'.
899 showPageInfo: function showPageInfo(verbose, sections) {
900 let { commandline, dactyl, options } = this.modules;
904 // Ctrl-g single line output
906 let file = this.win.location.pathname.split("/").pop() || _("buffer.noName");
907 let title = this.win.document.title || _("buffer.noTitle");
909 let info = template.map(
910 (sections || options["pageinfo"])
911 .map(function (opt) Buffer.pageInfo[opt].action.call(self)),
912 function (res) res && iter(res).join(", ") || undefined,
915 if (bookmarkcache.isBookmarked(this.URL))
916 info += ", " + _("buffer.bookmarked");
918 let pageInfoText = <>{file.quote()} [{info}] {title}</>;
919 dactyl.echo(pageInfoText, commandline.FORCE_SINGLELINE);
923 let list = template.map(sections || options["pageinfo"], function (option) {
924 let { action, title } = Buffer.pageInfo[option];
925 return template.table(title, action.call(self, true));
928 commandline.commandOutput(list);
932 * Stops loading and animations in the current content.
934 stop: function stop() {
935 let { config } = this.modules;
940 this.docShell.stop(this.docShell.STOP_ALL);
944 * Opens a viewer to inspect the source of the currently selected
947 viewSelectionSource: function viewSelectionSource() {
948 // copied (and tuned somewhat) from browser.jar -> nsContextMenu.js
949 let { document, window } = this.topWindow;
951 let win = document.commandDispatcher.focusedWindow;
952 if (win == this.topWindow)
953 win = this.focusedFrame;
955 let charset = win ? "charset=" + win.document.characterSet : null;
957 window.openDialog("chrome://global/content/viewPartialSource.xul",
958 "_blank", "scrollbars,resizable,chrome,dialog=no",
959 null, charset, win.getSelection(), "selection");
963 * Opens a viewer to inspect the source of the current buffer or the
964 * specified *url*. Either the default viewer or the configured external
967 * @param {string|object|null} loc If a string, the URL of the source,
968 * otherwise an object with some or all of the following properties:
970 * url: The URL to view.
971 * doc: The document to view.
972 * line: The line to select.
973 * column: The column to select.
975 * If no URL is provided, the current document is used.
976 * @default The current buffer.
977 * @param {boolean} useExternalEditor View the source in the external editor.
979 viewSource: function viewSource(loc, useExternalEditor) {
980 let { dactyl, editor, history, options } = this.modules;
982 let window = this.topWindow;
984 let doc = this.focusedFrame.document;
987 if (options.get("editor").has("line") || !loc.url)
988 this.viewSourceExternally(loc.doc || loc.url || doc, loc);
990 window.openDialog("chrome://global/content/viewSource.xul",
991 "_blank", "all,dialog=no",
992 loc.url, null, null, loc.line);
995 if (useExternalEditor)
996 this.viewSourceExternally(loc || doc);
998 let url = loc || doc.location.href;
999 const PREFIX = "view-source:";
1000 if (url.indexOf(PREFIX) == 0)
1001 url = url.substr(PREFIX.length);
1005 let sh = history.session;
1006 if (sh[sh.index].URI.spec == url)
1007 this.docShell.gotoIndex(sh.index);
1009 dactyl.open(url, { hide: true });
1015 * Launches an editor to view the source of the given document. The
1016 * contents of the document are saved to a temporary local file and
1017 * removed when the editor returns. This function returns
1020 * @param {Document} doc The document to view.
1021 * @param {function|object} callback If a function, the callback to be
1022 * called with two arguments: the nsIFile of the file, and temp, a
1023 * boolean which is true if the file is temporary. Otherwise, an object
1024 * with line and column properties used to determine where to open the
1028 viewSourceExternally: Class("viewSourceExternally",
1029 XPCOM([Ci.nsIWebProgressListener, Ci.nsISupportsWeakReference]), {
1030 init: function init(doc, callback) {
1031 this.callback = callable(callback) ? callback :
1032 function (file, temp) {
1033 let { editor } = overlay.activeModules;
1035 editor.editFileExternally(update({ file: file.path }, callback || {}),
1036 function () { temp && file.remove(false); });
1040 let uri = isString(doc) ? util.newURI(doc) : util.newURI(doc.location.href);
1041 let ext = uri.fileExtension || "txt";
1042 if (doc.contentType)
1043 ext = services.mime.getPrimaryExtension(doc.contentType, ext);
1046 return io.withTempFiles(function (temp) {
1047 let encoder = services.HtmlEncoder();
1048 encoder.init(doc, "text/unicode", encoder.OutputRaw|encoder.OutputPreformatted);
1049 temp.write(encoder.encodeToString(), ">");
1050 return this.callback(temp, true);
1051 }, this, true, ext);
1053 let file = util.getFile(uri);
1055 this.callback(file, false);
1057 this.file = io.createTempFile();
1058 var persist = services.Persist();
1059 persist.persistFlags = persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
1060 persist.progressListener = this;
1061 persist.saveURI(uri, null, null, null, null, this.file);
1066 onStateChange: function onStateChange(progress, request, flags, status) {
1067 if ((flags & this.STATE_STOP) && status == 0) {
1069 var ok = this.callback(this.file, true);
1073 this.file.remove(false);
1081 * Increases the zoom level of the current buffer.
1083 * @param {number} steps The number of zoom levels to jump.
1084 * @param {boolean} fullZoom Whether to use full zoom or text zoom.
1086 zoomIn: function zoomIn(steps, fullZoom) {
1087 this.bumpZoomLevel(steps, fullZoom);
1091 * Decreases the zoom level of the current buffer.
1093 * @param {number} steps The number of zoom levels to jump.
1094 * @param {boolean} fullZoom Whether to use full zoom or text zoom.
1096 zoomOut: function zoomOut(steps, fullZoom) {
1097 this.bumpZoomLevel(-steps, fullZoom);
1101 * Adjusts the page zoom of the current buffer to the given absolute
1104 * @param {number} value The new zoom value as a possibly fractional
1105 * percentage of the page's natural size.
1106 * @param {boolean} fullZoom If true, zoom all content of the page,
1107 * including raster images. If false, zoom only text. If omitted,
1108 * use the current zoom function. @optional
1109 * @throws {FailedAssertion} if the given *value* is not within the
1110 * closed range [Buffer.ZOOM_MIN, Buffer.ZOOM_MAX].
1112 setZoom: function setZoom(value, fullZoom) {
1113 let { dactyl, statusline } = this.modules;
1114 let { ZoomManager } = this;
1116 if (fullZoom === undefined)
1117 fullZoom = ZoomManager.useFullZoom;
1119 ZoomManager.useFullZoom = fullZoom;
1123 this.contentViewer.textZoom = fullZoom ? 1 : value;
1124 this.contentViewer.fullZoom = !fullZoom ? 1 : value;
1126 catch (e if e == Cr.NS_ERROR_ILLEGAL_VALUE) {
1127 return dactyl.echoerr(_("zoom.illegal"));
1130 if (services.has("contentPrefs") && !storage.privateMode
1131 && prefs.get("browser.zoom.siteSpecific")) {
1132 services.contentPrefs[value != 1 ? "setPref" : "removePref"]
1133 (this.uri, "browser.content.full-zoom", value);
1134 services.contentPrefs[value != 1 ? "setPref" : "removePref"]
1135 (this.uri, "dactyl.content.full-zoom", fullZoom);
1138 statusline.updateZoomLevel();
1142 * Updates the zoom level of this buffer from a content preference.
1144 updateZoom: util.wrapCallback(function updateZoom() {
1148 if (services.has("contentPrefs") && prefs.get("browser.zoom.siteSpecific"))
1149 services.contentPrefs.getPref(uri, "dactyl.content.full-zoom", function (val) {
1150 if (val != null && uri.equals(self.uri) && val != prefs.get("browser.zoom.full"))
1151 [self.contentViewer.textZoom, self.contentViewer.fullZoom] =
1152 [self.contentViewer.fullZoom, self.contentViewer.textZoom];
1157 * Adjusts the page zoom of the current buffer relative to the
1158 * current zoom level.
1160 * @param {number} steps The integral number of natural fractions by which
1161 * to adjust the current page zoom. If positive, the zoom level is
1162 * increased, if negative it is decreased.
1163 * @param {boolean} fullZoom If true, zoom all content of the page,
1164 * including raster images. If false, zoom only text. If omitted, use
1165 * the current zoom function. @optional
1166 * @throws {FailedAssertion} if the buffer's zoom level is already at its
1167 * extreme in the given direction.
1169 bumpZoomLevel: function bumpZoomLevel(steps, fullZoom) {
1170 let { ZoomManager } = this;
1172 if (fullZoom === undefined)
1173 fullZoom = ZoomManager.useFullZoom;
1175 let values = ZoomManager.zoomValues;
1176 let cur = values.indexOf(ZoomManager.snap(this.zoomLevel / 100));
1177 let i = Math.constrain(cur + steps, 0, values.length - 1);
1179 util.assert(i != cur || fullZoom != ZoomManager.useFullZoom);
1181 this.setZoom(Math.round(values[i] * 100), fullZoom);
1184 getAllFrames: deprecated("buffer.allFrames", "allFrames"),
1185 scrollTop: deprecated("buffer.scrollToPercent", function scrollTop() this.scrollToPercent(null, 0)),
1186 scrollBottom: deprecated("buffer.scrollToPercent", function scrollBottom() this.scrollToPercent(null, 100)),
1187 scrollStart: deprecated("buffer.scrollToPercent", function scrollStart() this.scrollToPercent(0, null)),
1188 scrollEnd: deprecated("buffer.scrollToPercent", function scrollEnd() this.scrollToPercent(100, null)),
1189 scrollColumns: deprecated("buffer.scrollHorizontal", function scrollColumns(cols) this.scrollHorizontal("columns", cols)),
1190 scrollPages: deprecated("buffer.scrollHorizontal", function scrollPages(pages) this.scrollVertical("pages", pages)),
1191 scrollTo: deprecated("Buffer.scrollTo", function scrollTo(x, y) this.win.scrollTo(x, y)),
1192 textZoom: deprecated("buffer.zoomValue/buffer.fullZoom", function textZoom() this.contentViewer.markupDocumentViewer.textZoom * 100)
1194 PageInfo: Struct("PageInfo", "name", "title", "action")
1200 * Adds a new section to the page information output.
1202 * @param {string} option The section's value in 'pageinfo'.
1203 * @param {string} title The heading for this section's
1205 * @param {function} func The function to generate this
1208 addPageInfoSection: function addPageInfoSection(option, title, func) {
1209 this.pageInfo[option] = Buffer.PageInfo(option, title, func);
1212 Scrollable: function Scrollable(elem) {
1213 if (elem instanceof Ci.nsIDOMElement)
1215 if (isinstance(elem, [Ci.nsIDOMWindow, Ci.nsIDOMDocument]))
1217 __proto__: elem.documentElement || elem.ownerDocument.documentElement,
1219 win: elem.defaultView || elem.ownerDocument.defaultView,
1221 get clientWidth() this.win.innerWidth,
1222 get clientHeight() this.win.innerHeight,
1224 get scrollWidth() this.win.scrollMaxX + this.win.innerWidth,
1225 get scrollHeight() this.win.scrollMaxY + this.win.innerHeight,
1227 get scrollLeft() this.win.scrollX,
1228 set scrollLeft(val) { this.win.scrollTo(val, this.win.scrollY) },
1230 get scrollTop() this.win.scrollY,
1231 set scrollTop(val) { this.win.scrollTo(this.win.scrollX, val) }
1236 get ZOOM_MIN() prefs.get("zoom.minPercent"),
1237 get ZOOM_MAX() prefs.get("zoom.maxPercent"),
1239 setZoom: deprecated("buffer.setZoom", function setZoom()
1240 let ({ buffer } = overlay.activeModules) buffer.setZoom.apply(buffer, arguments)),
1241 bumpZoomLevel: deprecated("buffer.bumpZoomLevel", function bumpZoomLevel()
1242 let ({ buffer } = overlay.activeModules) buffer.bumpZoomLevel.apply(buffer, arguments)),
1245 * Returns the currently selected word in *win*. If the selection is
1246 * null, it tries to guess the word that the caret is positioned in.
1250 currentWord: function currentWord(win, select) {
1251 let { Editor, options } = Buffer(win).modules;
1253 let selection = win.getSelection();
1254 if (selection.rangeCount == 0)
1257 let range = selection.getRangeAt(0).cloneRange();
1258 if (range.collapsed) {
1259 let re = options.get("iskeyword").regexp;
1260 Editor.extendRange(range, true, re, true);
1261 Editor.extendRange(range, false, re, true);
1264 selection.removeAllRanges();
1265 selection.addRange(range);
1267 return DOM.stringify(range);
1270 getDefaultNames: function getDefaultNames(node) {
1271 let url = node.href || node.src || node.documentURI;
1272 let currExt = url.replace(/^.*?(?:\.([a-z0-9]+))?$/i, "$1").toLowerCase();
1275 if (isinstance(node, [Ci.nsIDOMDocument,
1276 Ci.nsIDOMHTMLImageElement])) {
1277 let type = node.contentType || node.QueryInterface(Ci.nsIImageLoadingContent)
1278 .getRequest(0).mimeType;
1280 if (type === "text/plain")
1281 ext = "." + (currExt || "txt");
1283 ext = "." + services.mime.getPrimaryExtension(type, currExt);
1286 ext = "." + currExt;
1288 let re = ext ? RegExp("(\\." + currExt + ")?$", "i") : /$/;
1292 names.push([node.title,
1293 _("buffer.save.pageName")]);
1296 names.push([node.alt,
1297 _("buffer.save.altText")]);
1299 if (!isinstance(node, Ci.nsIDOMDocument) && node.textContent)
1300 names.push([node.textContent,
1301 _("buffer.save.linkText")]);
1303 names.push([decodeURIComponent(url.replace(/.*?([^\/]*)\/*$/, "$1")),
1304 _("buffer.save.filename")]);
1306 return names.filter(function ([leaf, title]) leaf)
1307 .map(function ([leaf, title]) [leaf.replace(config.OS.illegalCharacters, encodeURIComponent)
1308 .replace(re, ext), title]);
1311 findScrollableWindow: deprecated("buffer.findScrollableWindow", function findScrollableWindow()
1312 let ({ buffer } = overlay.activeModules) buffer.findScrollableWindow.apply(buffer, arguments)),
1313 findScrollable: deprecated("buffer.findScrollable", function findScrollable()
1314 let ({ buffer } = overlay.activeModules) buffer.findScrollable.apply(buffer, arguments)),
1316 isScrollable: function isScrollable(elem, dir, horizontal) {
1317 if (!DOM(elem).isScrollable(horizontal ? "horizontal" : "vertical"))
1320 return this.canScroll(elem, dir, horizontal);
1323 canScroll: function canScroll(elem, dir, horizontal) {
1324 let pos = "scrollTop", size = "clientHeight", max = "scrollHeight", layoutSize = "offsetHeight",
1325 overflow = "overflowX", border1 = "borderTopWidth", border2 = "borderBottomWidth";
1327 pos = "scrollLeft", size = "clientWidth", max = "scrollWidth", layoutSize = "offsetWidth",
1328 overflow = "overflowX", border1 = "borderLeftWidth", border2 = "borderRightWidth";
1330 let style = DOM(elem).style;
1331 let borderSize = Math.round(parseFloat(style[border1]) + parseFloat(style[border2]));
1332 let realSize = elem[size];
1334 // Stupid Gecko eccentricities. May fail for quirks mode documents.
1335 if (elem[size] + borderSize == elem[max] || elem[size] == 0) // Stupid, fallible heuristic.
1338 if (style[overflow] == "hidden")
1339 realSize += borderSize;
1340 return dir < 0 && elem[pos] > 0 || dir > 0 && elem[pos] + realSize < elem[max] || !dir && realSize < elem[max];
1344 * Scroll the contents of the given element to the absolute *left*
1345 * and *top* pixel offsets.
1347 * @param {Element} elem The element to scroll.
1348 * @param {number|null} left The left absolute pixel offset. If
1349 * null, to not alter the horizontal scroll offset.
1350 * @param {number|null} top The top absolute pixel offset. If
1351 * null, to not alter the vertical scroll offset.
1352 * @param {string} reason The reason for the scroll event. See
1353 * {@link marks.push}. @optional
1355 scrollTo: function scrollTo(elem, left, top, reason) {
1356 let doc = elem.ownerDocument || elem.document || elem;
1358 let { buffer, marks, options } = util.topWindow(doc.defaultView).dactyl.modules;
1360 if (~[elem, elem.document, elem.ownerDocument].indexOf(buffer.focusedFrame.document))
1363 if (options["scrollsteps"] > 1)
1364 return this.smoothScrollTo(elem, left, top);
1366 elem = Buffer.Scrollable(elem);
1368 elem.scrollLeft = left;
1370 elem.scrollTop = top;
1374 * Like scrollTo, but scrolls more smoothly and does not update
1377 smoothScrollTo: function smoothScrollTo(node, x, y) {
1378 let { options } = overlay.activeModules;
1380 let time = options["scrolltime"];
1381 let steps = options["scrollsteps"];
1383 let elem = Buffer.Scrollable(node);
1385 if (node.dactylScrollTimer)
1386 node.dactylScrollTimer.cancel();
1389 x = elem.scrollLeft;
1393 x = node.dactylScrollDestX = Math.min(x, elem.scrollWidth - elem.clientWidth);
1394 y = node.dactylScrollDestY = Math.min(y, elem.scrollHeight - elem.clientHeight);
1395 let [startX, startY] = [elem.scrollLeft, elem.scrollTop];
1398 if (n++ === steps) {
1399 elem.scrollLeft = x;
1401 delete node.dactylScrollDestX;
1402 delete node.dactylScrollDestY;
1405 elem.scrollLeft = startX + (x - startX) / steps * n;
1406 elem.scrollTop = startY + (y - startY) / steps * n;
1407 node.dactylScrollTimer = util.timeout(next, time / steps);
1413 * Scrolls the currently given element horizontally.
1415 * @param {Element} elem The element to scroll.
1416 * @param {string} unit The increment by which to scroll.
1417 * Possible values are: "columns", "pages"
1418 * @param {number} number The possibly fractional number of
1419 * increments to scroll. Positive values scroll to the right while
1420 * negative values scroll to the left.
1421 * @throws {FailedAssertion} if scrolling is not possible in the
1424 scrollHorizontal: function scrollHorizontal(node, unit, number) {
1425 let fontSize = parseInt(DOM(node).style.fontSize);
1427 let elem = Buffer.Scrollable(node);
1429 if (unit == "columns")
1430 increment = fontSize; // Good enough, I suppose.
1431 else if (unit == "pages")
1432 increment = elem.clientWidth - fontSize;
1436 util.assert(number < 0 ? elem.scrollLeft > 0 : elem.scrollLeft < elem.scrollWidth - elem.clientWidth);
1438 let left = node.dactylScrollDestX !== undefined ? node.dactylScrollDestX : elem.scrollLeft;
1439 node.dactylScrollDestX = undefined;
1441 Buffer.scrollTo(node, left + number * increment, null, "h-" + unit);
1445 * Scrolls the given element vertically.
1447 * @param {Element} elem The element to scroll.
1448 * @param {string} unit The increment by which to scroll.
1449 * Possible values are: "lines", "pages"
1450 * @param {number} number The possibly fractional number of
1451 * increments to scroll. Positive values scroll upward while
1452 * negative values scroll downward.
1453 * @throws {FailedAssertion} if scrolling is not possible in the
1456 scrollVertical: function scrollVertical(node, unit, number) {
1457 let fontSize = parseInt(DOM(node).style.lineHeight);
1459 let elem = Buffer.Scrollable(node);
1461 if (unit == "lines")
1462 increment = fontSize;
1463 else if (unit == "pages")
1464 increment = elem.clientHeight - fontSize;
1468 util.assert(number < 0 ? elem.scrollTop > 0 : elem.scrollTop < elem.scrollHeight - elem.clientHeight);
1470 let top = node.dactylScrollDestY !== undefined ? node.dactylScrollDestY : elem.scrollTop;
1471 node.dactylScrollDestY = undefined;
1473 Buffer.scrollTo(node, null, top + number * increment, "v-" + unit);
1477 * Scrolls the currently active element to the given horizontal and
1478 * vertical percentages.
1480 * @param {Element} elem The element to scroll.
1481 * @param {number|null} horizontal The possibly fractional
1482 * percentage of the current viewport width to scroll to. If null,
1483 * do not scroll horizontally.
1484 * @param {number|null} vertical The possibly fractional percentage
1485 * of the current viewport height to scroll to. If null, do not
1486 * scroll vertically.
1488 scrollToPercent: function scrollToPercent(node, horizontal, vertical) {
1489 let elem = Buffer.Scrollable(node);
1490 Buffer.scrollTo(node,
1491 horizontal == null ? null
1492 : (elem.scrollWidth - elem.clientWidth) * (horizontal / 100),
1493 vertical == null ? null
1494 : (elem.scrollHeight - elem.clientHeight) * (vertical / 100));
1498 * Scrolls the currently active element to the given horizontal and
1499 * vertical position.
1501 * @param {Element} elem The element to scroll.
1502 * @param {number|null} horizontal The possibly fractional
1503 * line ordinal to scroll to.
1504 * @param {number|null} vertical The possibly fractional
1505 * column ordinal to scroll to.
1507 scrollToPosition: function scrollToPosition(elem, horizontal, vertical) {
1508 let style = DOM(elem.body || elem).style;
1509 Buffer.scrollTo(elem,
1510 horizontal == null ? null :
1511 horizontal == 0 ? 0 : this._exWidth(elem) * horizontal,
1512 vertical == null ? null : parseFloat(style.lineHeight) * vertical);
1516 * Returns the current scroll position as understood by
1517 * {@link #scrollToPosition}.
1519 * @param {Element} elem The element to scroll.
1521 getScrollPosition: function getPosition(node) {
1522 let style = DOM(node.body || node).style;
1524 let elem = Buffer.Scrollable(node);
1526 x: elem.scrollLeft && elem.scrollLeft / this._exWidth(node),
1527 y: elem.scrollTop / parseFloat(style.lineHeight)
1531 _exWidth: function _exWidth(elem) {
1533 let div = DOM(<elem style="width: 1ex !important; position: absolute !important; padding: 0 !important; display: block;"/>,
1534 elem.ownerDocument).appendTo(elem.body || elem);
1536 return parseFloat(div.style.width);
1543 return parseFloat(DOM(elem).fontSize) / 1.618;
1547 openUploadPrompt: function openUploadPrompt(elem) {
1548 let { io } = overlay.activeModules;
1550 io.CommandFileMode(_("buffer.prompt.uploadFile") + " ", {
1551 onSubmit: function onSubmit(path) {
1552 let file = io.File(path);
1553 util.assert(file.exists());
1555 DOM(elem).val(file.path).change();
1557 }).open(elem.value);
1560 init: function init(dactyl, modules, window) {
1561 init.superapply(this, arguments);
1563 dactyl.commands["buffer.viewSource"] = function (event) {
1564 let elem = event.originalTarget;
1565 let obj = { url: elem.getAttribute("href"), line: Number(elem.getAttribute("line")) };
1566 if (elem.hasAttribute("column"))
1567 obj.column = elem.getAttribute("column");
1569 modules.buffer.viewSource(obj);
1572 commands: function initCommands(dactyl, modules, window) {
1573 let { buffer, commands, config, options } = modules;
1575 commands.add(["frameo[nly]"],
1576 "Show only the current frame's page",
1578 dactyl.open(buffer.focusedFrame.location.href);
1582 commands.add(["ha[rdcopy]"],
1583 "Print current document",
1587 // FIXME: arg handling is a bit of a mess, check for filename
1588 dactyl.assert(!arg || arg[0] == ">" && !config.OS.isWindows,
1589 _("error.trailingCharacters"));
1591 const PRINTER = "PostScript/default";
1592 const BRANCH = "printer_" + PRINTER + ".";
1593 const BRANCHES = ["print.", BRANCH, "print." + BRANCH];
1594 function set(pref, value) {
1595 BRANCHES.forEach(function (branch) { prefs.set(branch + pref, value) });
1598 prefs.withContext(function () {
1600 prefs.set("print.print_printer", PRINTER);
1602 set("print_to_file", true);
1603 set("print_to_filename", io.File(arg.substr(1)).path);
1605 dactyl.echomsg(_("print.toFile", arg.substr(1)));
1608 dactyl.echomsg(_("print.sending"));
1610 prefs.set("print.always_print_silent", args.bang);
1612 prefs.set("print.show_print_progress", !args.bang);
1614 config.browser.contentWindow.print();
1617 dactyl.echomsg(_("print.sent"));
1622 completer: function (context, args) {
1623 if (args.bang && /^>/.test(context.filter))
1624 context.fork("file", 1, modules.completion, "file");
1629 commands.add(["pa[geinfo]"],
1630 "Show various page information",
1633 let opt = options.get("pageinfo");
1635 dactyl.assert(!arg || opt.validator(opt.parse(arg)),
1636 _("error.invalidArgument", arg));
1637 buffer.showPageInfo(true, arg);
1641 completer: function (context) {
1642 modules.completion.optionValue(context, "pageinfo", "+", "");
1643 context.title = ["Page Info"];
1647 commands.add(["pagest[yle]", "pas"],
1648 "Select the author style sheet to apply",
1650 let arg = args[0] || "";
1652 let titles = buffer.alternateStyleSheets.map(function (stylesheet) stylesheet.title);
1654 dactyl.assert(!arg || titles.indexOf(arg) >= 0,
1655 _("error.invalidArgument", arg));
1657 if (options["usermode"])
1658 options["usermode"] = false;
1660 window.stylesheetSwitchAll(buffer.focusedFrame, arg);
1664 completer: function (context) modules.completion.alternateStyleSheet(context),
1668 commands.add(["re[load]"],
1669 "Reload the current web page",
1670 function (args) { modules.tabs.reload(config.browser.mCurrentTab, args.bang); },
1676 // TODO: we're prompted if download.useDownloadDir isn't set and no arg specified - intentional?
1677 commands.add(["sav[eas]", "w[rite]"],
1678 "Save current document to disk",
1680 let { commandline, io } = modules;
1681 let { doc, win } = buffer;
1683 let chosenData = null;
1684 let filename = args[0];
1686 let command = commandline.command;
1688 if (filename[0] == "!")
1689 return buffer.viewSourceExternally(buffer.focusedFrame.document,
1691 let output = io.system(filename.substr(1), file);
1692 commandline.command = command;
1693 commandline.commandOutput(<span highlight="CmdOutput">{output}</span>);
1696 if (/^>>/.test(filename)) {
1697 let file = io.File(filename.replace(/^>>\s*/, ""));
1698 dactyl.assert(args.bang || file.exists() && file.isWritable(),
1699 _("io.notWriteable", file.path.quote()));
1701 return buffer.viewSourceExternally(buffer.focusedFrame.document,
1702 function (tmpFile) {
1704 file.write(tmpFile, ">>");
1707 dactyl.echoerr(_("io.notWriteable", file.path.quote()));
1712 let file = io.File(filename);
1714 if (filename.substr(-1) === File.PATH_SEP || file.exists() && file.isDirectory())
1715 file.append(Buffer.getDefaultNames(doc)[0][0]);
1717 dactyl.assert(args.bang || !file.exists(), _("io.exists"));
1719 chosenData = { file: file, uri: util.newURI(doc.location.href) };
1722 // if browser.download.useDownloadDir = false then the "Save As"
1723 // dialog is used with this as the default directory
1724 // TODO: if we're going to do this shouldn't it be done in setCWD or the value restored?
1725 prefs.set("browser.download.lastDir", io.cwd.path);
1728 var contentDisposition = win.QueryInterface(Ci.nsIInterfaceRequestor)
1729 .getInterface(Ci.nsIDOMWindowUtils)
1730 .getDocumentMetadata("content-disposition");
1734 window.internalSave(doc.location.href, doc, null, contentDisposition,
1735 doc.contentType, false, null, chosenData,
1736 doc.referrer ? window.makeURI(doc.referrer) : null,
1742 completer: function (context) {
1743 let { buffer, completion } = modules;
1745 if (context.filter[0] == "!")
1747 if (/^>>/.test(context.filter))
1748 context.advance(/^>>\s*/.exec(context.filter)[0].length);
1750 completion.savePage(context, buffer.doc);
1751 context.fork("file", 0, completion, "file");
1756 commands.add(["st[op]"],
1757 "Stop loading the current web page",
1758 function () { buffer.stop(); },
1761 commands.add(["vie[wsource]"],
1762 "View source code of current document",
1763 function (args) { buffer.viewSource(args[0], args.bang); },
1767 completer: function (context) modules.completion.url(context, "bhf")
1770 commands.add(["zo[om]"],
1771 "Set zoom value of current web page",
1778 else if (/^\d+$/.test(arg))
1779 level = parseInt(arg, 10);
1780 else if (/^[+-]\d+$/.test(arg))
1781 level = Math.round(buffer.zoomLevel + parseInt(arg, 10));
1783 dactyl.assert(false, _("error.trailingCharacters"));
1785 buffer.setZoom(level, args.bang);
1792 completion: function initCompletion(dactyl, modules, window) {
1793 let { CompletionContext, buffer, completion } = modules;
1795 completion.alternateStyleSheet = function alternateStylesheet(context) {
1796 context.title = ["Stylesheet", "Location"];
1798 // unify split style sheets
1799 let styles = iter([s.title, []] for (s in values(buffer.alternateStyleSheets))).toObject();
1801 buffer.alternateStyleSheets.forEach(function (style) {
1802 styles[style.title].push(style.href || _("style.inline"));
1805 context.completions = [[title, href.join(", ")] for ([title, href] in Iterator(styles))];
1808 completion.savePage = function savePage(context, node) {
1809 context.fork("generated", context.filter.replace(/[^/]*$/, "").length,
1810 this, function (context) {
1811 context.generate = function () {
1812 this.incomplete = true;
1813 this.completions = Buffer.getDefaultNames(node);
1814 util.httpGet(node.href || node.src || node.documentURI, {
1816 callback: function callback(xhr) {
1817 context.incomplete = false;
1819 if (/filename="(.*?)"/.test(xhr.getResponseHeader("Content-Disposition")))
1820 context.completions.push([decodeURIComponent(RegExp.$1),
1821 _("buffer.save.suggested")]);
1824 context.completions = context.completions.slice();
1827 notificationCallbacks: Class(XPCOM([Ci.nsIChannelEventSink, Ci.nsIInterfaceRequestor]), {
1828 getInterface: function getInterface(iid) this.QueryInterface(iid),
1830 asyncOnChannelRedirect: function (oldChannel, newChannel, flags, callback) {
1831 if (newChannel instanceof Ci.nsIHttpChannel)
1832 newChannel.requestMethod = "HEAD";
1833 callback.onRedirectVerifyCallback(Cr.NS_OK);
1841 events: function initEvents(dactyl, modules, window) {
1842 let { buffer, config, events } = modules;
1844 events.listen(config.browser, "scroll", buffer.closure._updateBufferPosition, false);
1846 mappings: function initMappings(dactyl, modules, window) {
1847 let { Editor, Events, buffer, editor, events, ex, mappings, modes, options, tabs } = modules;
1849 mappings.add([modes.NORMAL],
1850 ["y", "<yank-location>"], "Yank current location to the clipboard",
1852 let { doc, uri } = buffer;
1853 if (uri instanceof Ci.nsIURL)
1854 uri.query = uri.query.replace(/(?:^|&)utm_[^&]+/g, "")
1857 let link = DOM("link[href][rev=canonical], link[href][rel=shortlink]", doc);
1858 let url = link.length && options.get("yankshort").getKey(uri) ? link.attr("href") : uri.spec;
1859 dactyl.clipboardWrite(url, true);
1862 mappings.add([modes.NORMAL],
1863 ["<C-a>", "<increment-url-path>"], "Increment last number in URL",
1864 function (args) { buffer.incrementURL(Math.max(args.count, 1)); },
1867 mappings.add([modes.NORMAL],
1868 ["<C-x>", "<decrement-url-path>"], "Decrement last number in URL",
1869 function (args) { buffer.incrementURL(-Math.max(args.count, 1)); },
1872 mappings.add([modes.NORMAL], ["gu", "<open-parent-path>"],
1873 "Go to parent directory",
1874 function (args) { buffer.climbUrlPath(Math.max(args.count, 1)); },
1877 mappings.add([modes.NORMAL], ["gU", "<open-root-path>"],
1878 "Go to the root of the website",
1879 function () { buffer.climbUrlPath(-1); });
1881 mappings.add([modes.COMMAND], [".", "<repeat-key>"],
1882 "Repeat the last key event",
1884 if (mappings.repeat) {
1885 for (let i in util.interruptibleRange(0, Math.max(args.count, 1), 100))
1891 mappings.add([modes.NORMAL], ["i", "<Insert>"],
1893 function () { modes.push(modes.CARET); });
1895 mappings.add([modes.NORMAL], ["<C-c>", "<stop-load>"],
1896 "Stop loading the current web page",
1897 function () { ex.stop(); });
1900 mappings.add([modes.NORMAL], ["j", "<Down>", "<C-e>", "<scroll-down-line>"],
1901 "Scroll document down",
1902 function (args) { buffer.scrollVertical("lines", Math.max(args.count, 1)); },
1905 mappings.add([modes.NORMAL], ["k", "<Up>", "<C-y>", "<scroll-up-line>"],
1906 "Scroll document up",
1907 function (args) { buffer.scrollVertical("lines", -Math.max(args.count, 1)); },
1910 mappings.add([modes.COMMAND], dactyl.has("mail") ? ["h", "<scroll-left-column>"] : ["h", "<Left>", "<scroll-left-column>"],
1911 "Scroll document to the left",
1912 function (args) { buffer.scrollHorizontal("columns", -Math.max(args.count, 1)); },
1915 mappings.add([modes.NORMAL], dactyl.has("mail") ? ["l", "<scroll-right-column>"] : ["l", "<Right>", "<scroll-right-column>"],
1916 "Scroll document to the right",
1917 function (args) { buffer.scrollHorizontal("columns", Math.max(args.count, 1)); },
1920 mappings.add([modes.NORMAL], ["0", "^", "<scroll-begin>"],
1921 "Scroll to the absolute left of the document",
1922 function () { buffer.scrollToPercent(0, null); });
1924 mappings.add([modes.NORMAL], ["$", "<scroll-end>"],
1925 "Scroll to the absolute right of the document",
1926 function () { buffer.scrollToPercent(100, null); });
1928 mappings.add([modes.NORMAL], ["gg", "<Home>", "<scroll-top>"],
1929 "Go to the top of the document",
1930 function (args) { buffer.scrollToPercent(null, args.count != null ? args.count : 0); },
1933 mappings.add([modes.NORMAL], ["G", "<End>", "<scroll-bottom>"],
1934 "Go to the end of the document",
1937 var elem = options.get("linenumbers")
1938 .getLine(buffer.focusedFrame.document,
1941 elem.scrollIntoView(true);
1942 else if (args.count)
1943 buffer.scrollToPosition(null, args.count);
1945 buffer.scrollToPercent(null, 100);
1949 mappings.add([modes.NORMAL], ["%", "<scroll-percent>"],
1950 "Scroll to {count} percent of the document",
1952 dactyl.assert(args.count > 0 && args.count <= 100);
1953 buffer.scrollToPercent(null, args.count);
1957 mappings.add([modes.NORMAL], ["<C-d>", "<scroll-down>"],
1958 "Scroll window downwards in the buffer",
1959 function (args) { buffer._scrollByScrollSize(args.count, true); },
1962 mappings.add([modes.NORMAL], ["<C-u>", "<scroll-up>"],
1963 "Scroll window upwards in the buffer",
1964 function (args) { buffer._scrollByScrollSize(args.count, false); },
1967 mappings.add([modes.NORMAL], ["<C-b>", "<PageUp>", "<S-Space>", "<scroll-up-page>"],
1968 "Scroll up a full page",
1969 function (args) { buffer.scrollVertical("pages", -Math.max(args.count, 1)); },
1972 mappings.add([modes.NORMAL], ["<Space>"],
1973 "Scroll down a full page",
1975 if (isinstance((services.focus.focusedWindow || buffer.win).document.activeElement,
1976 [Ci.nsIDOMHTMLInputElement,
1977 Ci.nsIDOMHTMLButtonElement,
1978 Ci.nsIDOMXULButtonElement]))
1981 buffer.scrollVertical("pages", Math.max(args.count, 1));
1985 mappings.add([modes.NORMAL], ["<C-f>", "<PageDown>", "<scroll-down-page>"],
1986 "Scroll down a full page",
1987 function (args) { buffer.scrollVertical("pages", Math.max(args.count, 1)); },
1990 mappings.add([modes.NORMAL], ["]f", "<previous-frame>"],
1992 function (args) { buffer.shiftFrameFocus(Math.max(args.count, 1)); },
1995 mappings.add([modes.NORMAL], ["[f", "<next-frame>"],
1996 "Focus previous frame",
1997 function (args) { buffer.shiftFrameFocus(-Math.max(args.count, 1)); },
2000 mappings.add([modes.NORMAL], ["["],
2001 "Jump to the previous element as defined by 'jumptags'",
2002 function (args) { buffer.findJump(args.arg, args.count, true); },
2003 { arg: true, count: true });
2005 mappings.add([modes.NORMAL], ["g]"],
2006 "Jump to the next off-screen element as defined by 'jumptags'",
2007 function (args) { buffer.findJump(args.arg, args.count, false, true); },
2008 { arg: true, count: true });
2010 mappings.add([modes.NORMAL], ["]"],
2011 "Jump to the next element as defined by 'jumptags'",
2012 function (args) { buffer.findJump(args.arg, args.count, false); },
2013 { arg: true, count: true });
2015 mappings.add([modes.NORMAL], ["{"],
2016 "Jump to the previous paragraph",
2017 function (args) { buffer.findJump("p", args.count, true); },
2020 mappings.add([modes.NORMAL], ["}"],
2021 "Jump to the next paragraph",
2022 function (args) { buffer.findJump("p", args.count, false); },
2025 mappings.add([modes.NORMAL], ["]]", "<next-page>"],
2026 "Follow the link labeled 'next' or '>' if it exists",
2028 buffer.findLink("next", options["nextpattern"], (args.count || 1) - 1, true);
2032 mappings.add([modes.NORMAL], ["[[", "<previous-page>"],
2033 "Follow the link labeled 'prev', 'previous' or '<' if it exists",
2035 buffer.findLink("prev", options["previouspattern"], (args.count || 1) - 1, true);
2039 mappings.add([modes.NORMAL], ["gf", "<view-source>"],
2040 "Toggle between rendered and source view",
2041 function () { buffer.viewSource(null, false); });
2043 mappings.add([modes.NORMAL], ["gF", "<view-source-externally>"],
2044 "View source with an external editor",
2045 function () { buffer.viewSource(null, true); });
2047 mappings.add([modes.NORMAL], ["gi", "<focus-input>"],
2048 "Focus last used input field",
2050 let elem = buffer.lastInputField;
2052 if (args.count >= 1 || !elem || !events.isContentNode(elem)) {
2053 let xpath = ["frame", "iframe", "input", "xul:textbox", "textarea[not(@disabled) and not(@readonly)]"];
2055 let frames = buffer.allFrames(null, true);
2057 let elements = array.flatten(frames.map(function (win) [m for (m in DOM.XPath(xpath, win.document))]))
2058 .filter(function (elem) {
2059 if (isinstance(elem, [Ci.nsIDOMHTMLFrameElement,
2060 Ci.nsIDOMHTMLIFrameElement]))
2061 return Editor.getEditor(elem.contentWindow);
2065 if (elem[0].readOnly || !DOM(elem).isEditable)
2068 let style = elem.style;
2069 let rect = elem.rect;
2070 return elem.isVisible &&
2071 (elem[0] instanceof Ci.nsIDOMXULTextBoxElement || style.MozUserFocus != "ignore") &&
2072 rect.width && rect.height;
2075 dactyl.assert(elements.length > 0);
2076 elem = elements[Math.constrain(args.count, 1, elements.length) - 1];
2078 buffer.focusElement(elem);
2079 DOM(elem).scrollIntoView();
2084 let url = dactyl.clipboardRead();
2085 dactyl.assert(url, _("error.clipboardEmpty"));
2087 let proto = /^([-\w]+):/.exec(url);
2088 if (proto && services.PROTOCOL + proto[1] in Cc && !RegExp(options["urlseparator"]).test(url))
2089 return url.replace(/\s+/g, "");
2093 mappings.add([modes.NORMAL], ["gP"],
2094 "Open (put) a URL based on the current clipboard contents in a new background buffer",
2096 dactyl.open(url(), { from: "paste", where: dactyl.NEW_TAB, background: true });
2099 mappings.add([modes.NORMAL], ["p", "<MiddleMouse>", "<open-clipboard-url>"],
2100 "Open (put) a URL based on the current clipboard contents in the current buffer",
2105 mappings.add([modes.NORMAL], ["P", "<tab-open-clipboard-url>"],
2106 "Open (put) a URL based on the current clipboard contents in a new buffer",
2108 dactyl.open(url(), { from: "paste", where: dactyl.NEW_TAB });
2112 mappings.add([modes.NORMAL], ["r", "<reload>"],
2113 "Reload the current web page",
2114 function () { tabs.reload(tabs.getTab(), false); });
2116 mappings.add([modes.NORMAL], ["R", "<full-reload>"],
2117 "Reload while skipping the cache",
2118 function () { tabs.reload(tabs.getTab(), true); });
2121 mappings.add([modes.NORMAL], ["Y", "<yank-selection>"],
2122 "Copy selected text or current word",
2124 let sel = buffer.currentWord;
2126 editor.setRegister(null, sel, true);
2130 mappings.add([modes.NORMAL], ["zi", "+", "<text-zoom-in>"],
2131 "Enlarge text zoom of current web page",
2132 function (args) { buffer.zoomIn(Math.max(args.count, 1), false); },
2135 mappings.add([modes.NORMAL], ["zm", "<text-zoom-more>"],
2136 "Enlarge text zoom of current web page by a larger amount",
2137 function (args) { buffer.zoomIn(Math.max(args.count, 1) * 3, false); },
2140 mappings.add([modes.NORMAL], ["zo", "-", "<text-zoom-out>"],
2141 "Reduce text zoom of current web page",
2142 function (args) { buffer.zoomOut(Math.max(args.count, 1), false); },
2145 mappings.add([modes.NORMAL], ["zr", "<text-zoom-reduce>"],
2146 "Reduce text zoom of current web page by a larger amount",
2147 function (args) { buffer.zoomOut(Math.max(args.count, 1) * 3, false); },
2150 mappings.add([modes.NORMAL], ["zz", "<text-zoom>"],
2151 "Set text zoom value of current web page",
2152 function (args) { buffer.setZoom(args.count > 1 ? args.count : 100, false); },
2155 mappings.add([modes.NORMAL], ["ZI", "zI", "<full-zoom-in>"],
2156 "Enlarge full zoom of current web page",
2157 function (args) { buffer.zoomIn(Math.max(args.count, 1), true); },
2160 mappings.add([modes.NORMAL], ["ZM", "zM", "<full-zoom-more>"],
2161 "Enlarge full zoom of current web page by a larger amount",
2162 function (args) { buffer.zoomIn(Math.max(args.count, 1) * 3, true); },
2165 mappings.add([modes.NORMAL], ["ZO", "zO", "<full-zoom-out>"],
2166 "Reduce full zoom of current web page",
2167 function (args) { buffer.zoomOut(Math.max(args.count, 1), true); },
2170 mappings.add([modes.NORMAL], ["ZR", "zR", "<full-zoom-reduce>"],
2171 "Reduce full zoom of current web page by a larger amount",
2172 function (args) { buffer.zoomOut(Math.max(args.count, 1) * 3, true); },
2175 mappings.add([modes.NORMAL], ["zZ", "<full-zoom>"],
2176 "Set full zoom value of current web page",
2177 function (args) { buffer.setZoom(args.count > 1 ? args.count : 100, true); },
2181 mappings.add([modes.NORMAL], ["<C-g>", "<page-info>"],
2182 "Print the current file name",
2183 function () { buffer.showPageInfo(false); });
2185 mappings.add([modes.NORMAL], ["g<C-g>", "<more-page-info>"],
2186 "Print file information",
2187 function () { buffer.showPageInfo(true); });
2189 options: function initOptions(dactyl, modules, window) {
2190 let { Option, buffer, completion, config, options } = modules;
2192 options.add(["encoding", "enc"],
2193 "The current buffer's character encoding",
2196 scope: Option.SCOPE_LOCAL,
2197 getter: function () buffer.docShell.QueryInterface(Ci.nsIDocCharset).charset,
2198 setter: function (val) {
2199 if (options["encoding"] == val)
2202 // Stolen from browser.jar/content/browser/browser.js, more or less.
2204 buffer.docShell.QueryInterface(Ci.nsIDocCharset).charset = val;
2205 window.PlacesUtils.history.setCharsetForURI(buffer.uri, val);
2206 buffer.docShell.reload(Ci.nsIWebNavigation.LOAD_FLAGS_CHARSET_CHANGE);
2208 catch (e) { dactyl.reportError(e); }
2211 completer: function (context) completion.charset(context)
2214 options.add(["iskeyword", "isk"],
2215 "Regular expression defining which characters constitute words",
2216 "string", '[^\\s.,!?:;/"\'^$%&?()[\\]{}<>#*+|=~_-]',
2218 setter: function (value) {
2219 this.regexp = util.regexp(value);
2222 validator: function (value) RegExp(value)
2225 options.add(["jumptags", "jt"],
2226 "XPath or CSS selector strings of jumpable elements for extended hint modes",
2228 "p": "p,table,ul,ol,blockquote",
2229 "h": "h1,h2,h3,h4,h5,h6"
2233 setter: function (vals) {
2234 for (let [k, v] in Iterator(vals))
2235 vals[k] = update(new String(v), { matcher: DOM.compileMatcher(Option.splitList(v)) });
2238 validator: function (value) DOM.validateMatcher.call(this, value)
2239 && Object.keys(value).every(function (v) v.length == 1)
2242 options.add(["linenumbers", "ln"],
2243 "Patterns used to determine line numbers used by G",
2245 // Make sure to update the docs when you change this.
2246 "view-source:*": 'body,[id^=line]',
2247 "code.google.com": '#nums [id^="nums_table"] a[href^="#"]',
2248 "github.com": '.line_numbers>*',
2249 "mxr.mozilla.org": 'a.l',
2250 "pastebin.com": '#code_frame>div>ol>li',
2251 "addons.mozilla.org": '.gutter>.line>a',
2252 "bugzilla.mozilla.org": ".bz_comment:not(.bz_first_comment):not(.ih_history)",
2253 "*": '/* Hgweb/Gitweb */ .completecodeline a.codeline, a.linenr'
2256 getLine: function getLine(doc, line) {
2257 let uri = util.newURI(doc.documentURI);
2258 for (let filter in values(this.value))
2259 if (filter(uri, doc)) {
2260 if (/^func:/.test(filter.result))
2261 var res = dactyl.userEval("(" + Option.dequote(filter.result.substr(5)) + ")")(doc, line);
2263 res = iter.nth(filter.matcher(doc),
2264 function (elem) (elem.nodeValue || elem.textContent).trim() == line && DOM(elem).display != "none",
2266 || iter.nth(filter.matcher(doc), util.identity, line - 1);
2276 setter: function (vals) {
2277 for (let value in values(vals))
2278 if (!/^func:/.test(value.result))
2279 value.matcher = DOM.compileMatcher(Option.splitList(value.result));
2283 validator: function validate(values) {
2284 return this.testValues(values, function (value) {
2285 if (/^func:/.test(value))
2286 return callable(dactyl.userEval("(" + Option.dequote(value.substr(5)) + ")"));
2288 return DOM.testMatcher(Option.dequote(value));
2293 options.add(["nextpattern"],
2294 "Patterns to use when guessing the next page in a document sequence",
2295 "regexplist", UTF8(/'^Next [>»]','^Next »','\bnext\b',^>$,^(>>|»)$,^(>|»),(>|»)$,'\bmore\b'/.source),
2296 { regexpFlags: "i" });
2298 options.add(["previouspattern"],
2299 "Patterns to use when guessing the previous page in a document sequence",
2300 "regexplist", UTF8(/'[<«] Prev$','« Prev$','\bprev(ious)?\b',^<$,^(<<|«)$,^(<|«),(<|«)$/.source),
2301 { regexpFlags: "i" });
2303 options.add(["pageinfo", "pa"],
2304 "Define which sections are shown by the :pageinfo command",
2305 "charlist", "gesfm",
2306 { get values() values(Buffer.pageInfo).toObject() });
2308 options.add(["scroll", "scr"],
2309 "Number of lines to scroll with <C-u> and <C-d> commands",
2311 { validator: function (value) value >= 0 });
2313 options.add(["showstatuslinks", "ssli"],
2314 "Where to show the destination of the link under the cursor",
2318 "": "Don't show link destinations",
2319 "status": "Show link destinations in the status line",
2320 "command": "Show link destinations in the command line"
2324 options.add(["scrolltime", "sct"],
2325 "The time, in milliseconds, in which to smooth scroll to a new position",
2328 options.add(["scrollsteps", "scs"],
2329 "The number of steps in which to smooth scroll to a new position",
2332 PREF: "general.smoothScroll",
2334 initValue: function () {},
2336 getter: function getter(value) !prefs.get(this.PREF) ? 1 : value,
2338 setter: function setter(value) {
2339 prefs.set(this.PREF, value > 1);
2344 validator: function (value) value > 0
2347 options.add(["usermode", "um"],
2348 "Show current website without styling defined by the author",
2351 setter: function (value) buffer.contentViewer.authorStyleDisabled = value,
2352 getter: function () buffer.contentViewer.authorStyleDisabled
2355 options.add(["yankshort", "ys"],
2356 "Yank the canonical short URL of a web page where provided",
2357 "sitelist", ["youtube.com", "bugzilla.mozilla.org"]);
2361 Buffer.addPageInfoSection("e", "Search Engines", function (verbose) {
2365 for (let { document: doc } in values(this.allFrames())) {
2366 let engines = DOM("link[href][rel=search][type='application/opensearchdescription+xml']", doc);
2367 nEngines += engines.length;
2370 for (let link in engines)
2371 yield [link.title || /*L*/ "Engine " + n++,
2372 <a xmlns={XHTML} href={link.href}
2373 onclick="if (event.button == 0) { window.external.AddSearchProvider(this.href); return false; }"
2374 highlight="URL">{link.href}</a>];
2377 if (!verbose && nEngines)
2378 yield nEngines + /*L*/" engine" + (nEngines > 1 ? "s" : "");
2381 Buffer.addPageInfoSection("f", "Feeds", function (verbose) {
2383 "application/rss+xml": "RSS",
2384 "application/atom+xml": "Atom",
2386 "application/xml": "XML",
2387 "application/rdf+xml": "XML"
2390 function isValidFeed(data, principal, isFeed) {
2391 if (!data || !principal)
2395 var type = data.type && data.type.toLowerCase();
2396 type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");
2398 isFeed = ["application/rss+xml", "application/atom+xml"].indexOf(type) >= 0 ||
2399 // really slimy: general XML types with magic letters in the title
2400 type in feedTypes && /\brss\b/i.test(data.title);
2405 services.security.checkLoadURIStrWithPrincipal(principal, data.href,
2406 services.security.DISALLOW_INHERIT_PRINCIPAL);
2420 for (let [i, win] in Iterator(this.allFrames())) {
2421 let doc = win.document;
2423 for (let link in DOM("link[href][rel=feed], link[href][rel=alternate][type]", doc)) {
2424 let rel = link.rel.toLowerCase();
2425 let feed = { title: link.title, href: link.href, type: link.type || "" };
2426 if (isValidFeed(feed, doc.nodePrincipal, rel == "feed")) {
2428 let type = feedTypes[feed.type] || "RSS";
2430 yield [feed.title, template.highlightURL(feed.href, true) + <span class="extra-info"> ({type})</span>];
2436 if (!verbose && nFeed)
2437 yield nFeed + /*L*/" feed" + (nFeed > 1 ? "s" : "");
2440 Buffer.addPageInfoSection("g", "General Info", function (verbose) {
2441 let doc = this.focusedFrame.document;
2444 const ACCESS_READ = Ci.nsICache.ACCESS_READ;
2445 let cacheKey = doc.documentURI;
2447 for (let proto in array.iterValues(["HTTP", "FTP"])) {
2449 var cacheEntryDescriptor = services.cache.createSession(proto, 0, true)
2450 .openCacheEntry(cacheKey, ACCESS_READ, false);
2456 let pageSize = []; // [0] bytes; [1] kbytes
2457 if (cacheEntryDescriptor) {
2458 pageSize[0] = util.formatBytes(cacheEntryDescriptor.dataSize, 0, false);
2459 pageSize[1] = util.formatBytes(cacheEntryDescriptor.dataSize, 2, true);
2460 if (pageSize[1] == pageSize[0])
2461 pageSize.length = 1; // don't output "xx Bytes" twice
2464 let lastModVerbose = new Date(doc.lastModified).toLocaleString();
2465 let lastMod = new Date(doc.lastModified).toLocaleFormat("%x %X");
2467 if (lastModVerbose == "Invalid Date" || new Date(doc.lastModified).getFullYear() == 1970)
2468 lastModVerbose = lastMod = null;
2472 yield (pageSize[1] || pageSize[0]) + /*L*/" bytes";
2477 yield ["Title", doc.title];
2478 yield ["URL", template.highlightURL(doc.location.href, true)];
2480 let ref = "referrer" in doc && doc.referrer;
2482 yield ["Referrer", template.highlightURL(ref, true)];
2485 yield ["File Size", pageSize[1] ? pageSize[1] + " (" + pageSize[0] + ")"
2488 yield ["Mime-Type", doc.contentType];
2489 yield ["Encoding", doc.characterSet];
2490 yield ["Compatibility", doc.compatMode == "BackCompat" ? "Quirks Mode" : "Full/Almost Standards Mode"];
2492 yield ["Last Modified", lastModVerbose];
2495 Buffer.addPageInfoSection("m", "Meta Tags", function (verbose) {
2499 // get meta tag data, sort and put into pageMeta[]
2500 let metaNodes = this.focusedFrame.document.getElementsByTagName("meta");
2502 return Array.map(metaNodes, function (node) [(node.name || node.httpEquiv), template.highlightURL(node.content)])
2503 .sort(function (a, b) util.compareIgnoreCase(a[0], b[0]));
2506 Buffer.addPageInfoSection("s", "Security", function (verbose) {
2507 let { statusline } = this.modules
2509 let identity = this.topWindow.gIdentityHandler;
2511 if (!verbose || !identity)
2514 // Modified from Firefox
2515 function location(data) array.compact([
2516 data.city, data.state, data.country
2519 switch (statusline.security) {
2522 var data = identity.getIdentityData();
2524 yield ["Host", identity.getEffectiveHost()];
2526 if (statusline.security === "extended")
2527 yield ["Owner", data.subjectOrg];
2529 yield ["Owner", _("pageinfo.s.ownerUnverified", data.subjectOrg)];
2531 if (location(data).length)
2532 yield ["Location", location(data)];
2534 yield ["Verified by", data.caOrg];
2536 if (identity._overrideService.hasMatchingOverride(identity._lastLocation.hostname,
2537 (identity._lastLocation.port || 443),
2539 yield ["User exception", /*L*/"true"];
2544 } catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
2548 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: