]> git.donarmstrong.com Git - dactyl.git/blobdiff - common/modules/buffer.jsm
Import r6923 from upstream hg supporting Firefox up to 22.0a1
[dactyl.git] / common / modules / buffer.jsm
index 357beea6556566c60eac0ce21dad1b9a82224b55..964c3c8166c4c6778202a58885c246498cb655d6 100644 (file)
@@ -1,22 +1,24 @@
 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
-// Copyright (c) 2008-2011 by Kris Maglione <maglione.k at Gmail>
+// Copyright (c) 2008-2013 Kris Maglione <maglione.k at Gmail>
 //
 // This work is licensed for reuse under an MIT license. Details are
 // given in the LICENSE.txt file included with this file.
-try {"use strict";
+"use strict";
 
-Components.utils.import("resource://dactyl/bootstrap.jsm");
 defineModule("buffer", {
     exports: ["Buffer", "buffer"],
     require: ["prefs", "services", "util"]
-}, this);
+});
 
-this.lazyRequire("finder", ["RangeFind"]);
-this.lazyRequire("io", ["io"]);
-this.lazyRequire("overlay", ["overlay"]);
-this.lazyRequire("storage", ["File", "storage"]);
-this.lazyRequire("template", ["template"]);
+lazyRequire("bookmarkcache", ["bookmarkcache"]);
+lazyRequire("contexts", ["Group"]);
+lazyRequire("io", ["io"]);
+lazyRequire("finder", ["RangeFind"]);
+lazyRequire("overlay", ["overlay"]);
+lazyRequire("sanitizer", ["sanitizer"]);
+lazyRequire("storage", ["File", "storage"]);
+lazyRequire("template", ["template"]);
 
 /**
  * A class to manage the primary web content buffer. The name comes
@@ -66,6 +68,45 @@ var Buffer = Module("Buffer", {
         );
     },
 
+    /**
+     * Gets a content preference for the given buffer.
+     *
+     * @param {string} pref The preference to get.
+     * @param {function(string|number|boolean)} callback The callback to
+     *      call with the preference value. @optional
+     * @returns {string|number|boolean} The value of the preference, if
+     *      callback is not provided.
+     */
+    getPref: function getPref(pref, callback) {
+        // God damn it.
+        if (config.haveGecko("19.0a1"))
+            services.contentPrefs.getPref(this.uri, pref,
+                                          sanitizer.getContext(this.win), callback);
+        else
+            services.contentPrefs.getPref(uri, pref, callback);
+    },
+
+    /**
+     * Sets a content preference for the given buffer.
+     *
+     * @param {string} pref The preference to set.
+     * @param {string} value The value to store.
+     */
+    setPref: function setPref(pref, value) {
+        services.contentPrefs.setPref(
+            this.uri, pref, value, sanitizer.getContext(this.win));
+    },
+
+    /**
+     * Clear a content preference for the given buffer.
+     *
+     * @param {string} pref The preference to clear.
+     */
+    clearPref: function clearPref(pref) {
+        services.contentPrefs.removePref(
+            this.uri, pref, sanitizer.getContext(this.win));
+    },
+
     climbUrlPath: function climbUrlPath(count) {
         let { dactyl } = this.modules;
 
@@ -234,8 +275,8 @@ var Buffer = Module("Buffer", {
         })(win || this.win);
 
         if (focusedFirst)
-            return frames.filter(function (f) f === this.focusedFrame).concat(
-                   frames.filter(function (f) f !== this.focusedFrame));
+            return frames.filter(function (f) f === this.focusedFrame, this).concat(
+                   frames.filter(function (f) f !== this.focusedFrame, this));
         return frames;
     },
 
@@ -574,6 +615,31 @@ var Buffer = Module("Buffer", {
      */
     get selectionController() util.selectionController(this.focusedFrame),
 
+    /**
+     * @property {string|null} The canonical short URL for the current
+     *      document.
+     */
+    get shortURL() {
+        let { uri, doc } = this;
+
+        for each (let shortener in Buffer.uriShorteners)
+            try {
+                let shortened = shortener(uri, doc);
+                if (shortened)
+                    return shortened.spec;
+            }
+            catch (e) {
+                util.reportError(e);
+            }
+
+        let link = DOM("link[href][rev=canonical], \
+                        link[href][rel=shortlink]", doc);
+        if (link)
+            return link.attr("href");
+
+        return null;
+    },
+
     /**
      * Opens the appropriate context menu for *elem*.
      *
@@ -585,8 +651,9 @@ var Buffer = Module("Buffer", {
      * Saves a page link to disk.
      *
      * @param {HTMLAnchorElement} elem The page link to save.
+     * @param {boolean} overwrite If true, overwrite any existing file.
      */
-    saveLink: function saveLink(elem) {
+    saveLink: function saveLink(elem, overwrite) {
         let { completion, dactyl, io } = this.modules;
 
         let self = this;
@@ -604,6 +671,8 @@ var Buffer = Module("Buffer", {
                     if (file.exists() && file.isDirectory())
                         file.append(Buffer.getDefaultNames(elem)[0][0]);
 
+                    util.assert(!file.exists() || overwrite, _("io.existsNoOverride", file.path));
+
                     try {
                         if (!file.exists())
                             file.create(File.NORMAL_FILE_TYPE, octal(644));
@@ -612,7 +681,7 @@ var Buffer = Module("Buffer", {
                         util.assert(false, _("save.invalidDestination", e.name));
                     }
 
-                    self.saveURI(uri, file);
+                    self.saveURI({ uri: uri, file: file, context: elem });
                 },
 
                 completer: function (context) completion.savePage(context, elem)
@@ -629,24 +698,33 @@ var Buffer = Module("Buffer", {
      * @param {nsIURI} uri The URI to save
      * @param {nsIFile} file The file into which to write the result.
      */
-    saveURI: function saveURI(uri, file, callback, self) {
+    saveURI: function saveURI(params) {
+        if (params instanceof Ci.nsIURI)
+            // Deprecated?
+            params = { uri: arguments[0], file: arguments[1],
+                       callback: arguments[2], self: arguments[3] };
+
         var persist = services.Persist();
         persist.persistFlags = persist.PERSIST_FLAGS_FROM_CACHE
                              | persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
 
         let window = this.topWindow;
+        let privacy = sanitizer.getContext(params.context || this.win);
+        let file = File(params.file);
         if (!file.exists())
             file.create(Ci.nsIFile.NORMAL_FILE_TYPE, octal(666));
 
         let downloadListener = new window.DownloadListener(window,
-                services.Transfer(uri, File(file).URI, "",
-                                  null, null, null, persist));
+                services.Transfer(params.uri, file.URI, "", null, null, null,
+                                  persist, privacy && privacy.usePrivateBrowsing));
 
+        var { callback, self } = params;
         if (callback)
             persist.progressListener = update(Object.create(downloadListener), {
                 onStateChange: util.wrapCallback(function onStateChange(progress, request, flags, status) {
                     if (callback && (flags & Ci.nsIWebProgressListener.STATE_STOP) && status == 0)
-                        util.trapErrors(callback, self, uri, file, progress, request, flags, status);
+                        util.trapErrors(callback, self, params.uri, file.file,
+                                        progress, request, flags, status);
 
                     return onStateChange.superapply(this, arguments);
                 })
@@ -654,7 +732,8 @@ var Buffer = Module("Buffer", {
         else
             persist.progressListener = downloadListener;
 
-        persist.saveURI(uri, null, null, null, null, file);
+        persist.saveURI(params.uri, null, null, null, null,
+                        file.file, privacy);
     },
 
     /**
@@ -748,8 +827,26 @@ var Buffer = Module("Buffer", {
             var sel = this.focusedFrame.getSelection();
         }
         catch (e) {}
+
         if (!elem && sel && sel.rangeCount)
             elem = sel.getRangeAt(0).startContainer;
+
+        if (!elem) {
+            let area = -1;
+            for (let e in DOM(Buffer.SCROLLABLE_SEARCH_SELECTOR,
+                              this.focusedFrame.document)) {
+                if (Buffer.isScrollable(e, dir, horizontal)) {
+                    let r = DOM(e).rect;
+                    let a = r.width * r.height;
+                    if (a > area) {
+                        area = a;
+                        elem = e;
+                    }
+                }
+            }
+            if (elem)
+                util.trapErrors("focus", elem);
+        }
         if (elem)
             elem = find(elem);
 
@@ -865,14 +962,14 @@ var Buffer = Module("Buffer", {
 
         // add the frame indicator
         let doc = frames[next].document;
-        let indicator = DOM(<div highlight="FrameIndicator"/>, doc)
+        let indicator = DOM(["div", { highlight: "FrameIndicator" }], doc)
                             .appendTo(doc.body || doc.documentElement || doc);
 
         util.timeout(function () { indicator.remove(); }, 500);
 
         // Doesn't unattach
-        //doc.body.setAttributeNS(NS.uri, "activeframe", "true");
-        //util.timeout(function () { doc.body.removeAttributeNS(NS.uri, "activeframe"); }, 500);
+        //doc.body.setAttributeNS(NS, "activeframe", "true");
+        //util.timeout(function () { doc.body.removeAttributeNS(NS, "activeframe"); }, 500);
     },
 
     // similar to pageInfo
@@ -885,8 +982,7 @@ var Buffer = Module("Buffer", {
     showElementInfo: function showElementInfo(elem) {
         let { dactyl } = this.modules;
 
-        XML.ignoreWhitespace = XML.prettyPrinting = false;
-        dactyl.echo(<><!--L-->Element:<br/>{util.objectToString(elem, true)}</>);
+        dactyl.echo(["", /*L*/"Element:", ["br"], util.objectToString(elem, true)]);
     },
 
     /**
@@ -910,12 +1006,12 @@ var Buffer = Module("Buffer", {
                 (sections || options["pageinfo"])
                     .map(function (opt) Buffer.pageInfo[opt].action.call(self)),
                 function (res) res && iter(res).join(", ") || undefined,
-                ", ");
+                ", ").join("");
 
             if (bookmarkcache.isBookmarked(this.URL))
                 info += ", " + _("buffer.bookmarked");
 
-            let pageInfoText = <>{file.quote()} [{info}] {title}</>;
+            let pageInfoText = [file.quote(), " [", info, "] ", title].join("");
             dactyl.echo(pageInfoText, commandline.FORCE_SINGLELINE);
             return;
         }
@@ -923,7 +1019,7 @@ var Buffer = Module("Buffer", {
         let list = template.map(sections || options["pageinfo"], function (option) {
             let { action, title } = Buffer.pageInfo[option];
             return template.table(title, action.call(self, true));
-        }, <br/>);
+        }, ["br"]);
 
         commandline.commandOutput(list);
     },
@@ -1037,7 +1133,15 @@ var Buffer = Module("Buffer", {
                     return true;
                 };
 
-            let uri = isString(doc) ? util.newURI(doc) : util.newURI(doc.location.href);
+            if (isString(doc)) {
+                var privacyContext = null;
+                var uri = util.newURI(doc);
+            }
+            else {
+                privacyContext = sanitizer.getContext(doc);
+                uri = util.newURI(doc.location.href);
+            }
+
             let ext = uri.fileExtension || "txt";
             if (doc.contentType)
                 ext = services.mime.getPrimaryExtension(doc.contentType, ext);
@@ -1058,7 +1162,8 @@ var Buffer = Module("Buffer", {
                 var persist = services.Persist();
                 persist.persistFlags = persist.PERSIST_FLAGS_REPLACE_EXISTING_FILES;
                 persist.progressListener = this;
-                persist.saveURI(uri, null, null, null, null, this.file);
+                persist.saveURI(uri, null, null, null, null, this.file,
+                                privacyContext);
             }
             return null;
         },
@@ -1110,7 +1215,7 @@ var Buffer = Module("Buffer", {
      *   closed range [Buffer.ZOOM_MIN, Buffer.ZOOM_MAX].
      */
     setZoom: function setZoom(value, fullZoom) {
-        let { dactyl, statusline } = this.modules;
+        let { dactyl, statusline, storage } = this.modules;
         let { ZoomManager } = this;
 
         if (fullZoom === undefined)
@@ -1127,12 +1232,16 @@ var Buffer = Module("Buffer", {
             return dactyl.echoerr(_("zoom.illegal"));
         }
 
-        if (services.has("contentPrefs") && !storage.privateMode
-                && prefs.get("browser.zoom.siteSpecific")) {
-            services.contentPrefs[value != 1 ? "setPref" : "removePref"]
-                (this.uri, "browser.content.full-zoom", value);
-            services.contentPrefs[value != 1 ? "setPref" : "removePref"]
-                (this.uri, "dactyl.content.full-zoom", fullZoom);
+        if (prefs.get("browser.zoom.siteSpecific")) {
+            var privacy = sanitizer.getContext(this.win);
+            if (value == 1) {
+                this.clearPref("browser.content.full-zoom");
+                this.clearPref("dactyl.content.full-zoom");
+            }
+            else {
+                this.setPref("browser.content.full-zoom", value);
+                this.setPref("dactyl.content.full-zoom", fullZoom);
+            }
         }
 
         statusline.updateZoomLevel();
@@ -1145,12 +1254,13 @@ var Buffer = Module("Buffer", {
         let self = this;
         let uri = this.uri;
 
-        if (services.has("contentPrefs") && prefs.get("browser.zoom.siteSpecific"))
-            services.contentPrefs.getPref(uri, "dactyl.content.full-zoom", function (val) {
+        if (prefs.get("browser.zoom.siteSpecific")) {
+            this.getPref("dactyl.content.full-zoom", function (val) {
                 if (val != null && uri.equals(self.uri) && val != prefs.get("browser.zoom.full"))
                     [self.contentViewer.textZoom, self.contentViewer.fullZoom] =
                         [self.contentViewer.fullZoom, self.contentViewer.textZoom];
             });
+        }
     }),
 
     /**
@@ -1191,6 +1301,12 @@ var Buffer = Module("Buffer", {
     scrollTo: deprecated("Buffer.scrollTo", function scrollTo(x, y) this.win.scrollTo(x, y)),
     textZoom: deprecated("buffer.zoomValue/buffer.fullZoom", function textZoom() this.contentViewer.markupDocumentViewer.textZoom * 100)
 }, {
+    /**
+     * The pattern used to search for a scrollable element when we have
+     * no starting point.
+     */
+    SCROLLABLE_SEARCH_SELECTOR: "html, body, div",
+
     PageInfo: Struct("PageInfo", "name", "title", "action")
                         .localize("title"),
 
@@ -1209,6 +1325,27 @@ var Buffer = Module("Buffer", {
         this.pageInfo[option] = Buffer.PageInfo(option, title, func);
     },
 
+    uriShorteners: [],
+
+    /**
+     * Adds a new URI shortener for documents matching the given filter.
+     *
+     * @param {string|function(URI, Document):boolean} filter A site filter
+     *      string or a function which accepts a URI and a document and
+     *      returns true if it can shorten the document's URI.
+     * @param {function(URI, Document):URI} shortener Returns a shortened
+     *      URL for the given URI and document.
+     */
+    addURIShortener: function addURIShortener(filter, shortener) {
+        if (isString(filter))
+            filter = Group.compileFilter(filter);
+
+        this.uriShorteners.push(function uriShortener(uri, doc) {
+            if (filter(uri, doc))
+                return shortener(uri, doc);
+        });
+    },
+
     Scrollable: function Scrollable(elem) {
         if (elem instanceof Ci.nsIDOMElement)
             return elem;
@@ -1224,6 +1361,9 @@ var Buffer = Module("Buffer", {
                 get scrollWidth() this.win.scrollMaxX + this.win.innerWidth,
                 get scrollHeight() this.win.scrollMaxY + this.win.innerHeight,
 
+                get scrollLeftMax() this.win.scrollMaxX,
+                get scrollRightMax() this.win.scrollMaxY,
+
                 get scrollLeft() this.win.scrollX,
                 set scrollLeft(val) { this.win.scrollTo(val, this.win.scrollY) },
 
@@ -1321,23 +1461,30 @@ var Buffer = Module("Buffer", {
     },
 
     canScroll: function canScroll(elem, dir, horizontal) {
-        let pos = "scrollTop", size = "clientHeight", max = "scrollHeight", layoutSize = "offsetHeight",
+        let pos = "scrollTop", size = "clientHeight", end = "scrollHeight", layoutSize = "offsetHeight",
             overflow = "overflowX", border1 = "borderTopWidth", border2 = "borderBottomWidth";
         if (horizontal)
-            pos = "scrollLeft", size = "clientWidth", max = "scrollWidth", layoutSize = "offsetWidth",
+            pos = "scrollLeft", size = "clientWidth", end = "scrollWidth", layoutSize = "offsetWidth",
             overflow = "overflowX", border1 = "borderLeftWidth", border2 = "borderRightWidth";
 
+        if (dir < 0)
+            return elem[pos] > 0;
+
+        let max = pos + "Max";
+        if (max in elem && pos > 0)
+            return elem[pos] < elem[max];
+
         let style = DOM(elem).style;
         let borderSize = Math.round(parseFloat(style[border1]) + parseFloat(style[border2]));
         let realSize = elem[size];
 
         // Stupid Gecko eccentricities. May fail for quirks mode documents.
-        if (elem[size] + borderSize == elem[max] || elem[size] == 0) // Stupid, fallible heuristic.
+        if (elem[size] + borderSize >= elem[end] || elem[size] == 0) // Stupid, fallible heuristic.
             return false;
 
         if (style[overflow] == "hidden")
             realSize += borderSize;
-        return dir < 0 && elem[pos] > 0 || dir > 0 && elem[pos] + realSize < elem[max] || !dir && realSize < elem[max];
+        return dir > 0 && elem[pos] + realSize < elem[end] || !dir && realSize < elem[end];
     },
 
     /**
@@ -1444,7 +1591,7 @@ var Buffer = Module("Buffer", {
     /**
      * Scrolls the given element vertically.
      *
-     * @param {Element} elem The element to scroll.
+     * @param {Node} node The node to scroll.
      * @param {string} unit The increment by which to scroll.
      *   Possible values are: "lines", "pages"
      * @param {number} number The possibly fractional number of
@@ -1530,7 +1677,7 @@ var Buffer = Module("Buffer", {
 
     _exWidth: function _exWidth(elem) {
         try {
-            let div = DOM(<elem style="width: 1ex !important; position: absolute !important; padding: 0 !important; display: block;"/>,
+            let div = DOM(["elem", { style: "width: 1ex !important; position: absolute !important; padding: 0 !important; display: block;" }],
                           elem.ownerDocument).appendTo(elem.body || elem);
             try {
                 return parseFloat(div.style.width);
@@ -1690,7 +1837,7 @@ var Buffer = Module("Buffer", {
                             function (file) {
                                 let output = io.system(filename.substr(1), file);
                                 commandline.command = command;
-                                commandline.commandOutput(<span highlight="CmdOutput">{output}</span>);
+                                commandline.commandOutput(["span", { highlight: "CmdOutput" }, output]);
                             });
 
                     if (/^>>/.test(filename)) {
@@ -1716,7 +1863,7 @@ var Buffer = Module("Buffer", {
 
                     dactyl.assert(args.bang || !file.exists(), _("io.exists"));
 
-                    chosenData = { file: file, uri: util.newURI(doc.location.href) };
+                    chosenData = { file: file.file, uri: util.newURI(doc.location.href) };
                 }
 
                 // if browser.download.useDownloadDir = false then the "Save As"
@@ -1734,7 +1881,7 @@ var Buffer = Module("Buffer", {
                 window.internalSave(doc.location.href, doc, null, contentDisposition,
                                     doc.contentType, false, null, chosenData,
                                     doc.referrer ? window.makeURI(doc.referrer) : null,
-                                    true);
+                                    doc, true);
             },
             {
                 argCount: "?",
@@ -1854,8 +2001,7 @@ var Buffer = Module("Buffer", {
                     uri.query = uri.query.replace(/(?:^|&)utm_[^&]+/g, "")
                                          .replace(/^&/, "");
 
-                let link = DOM("link[href][rev=canonical], link[href][rel=shortlink]", doc);
-                let url = link.length && options.get("yankshort").getKey(uri) ? link.attr("href") : uri.spec;
+                let url = options.get("yankshort").getKey(uri) && buffer.shortURL || uri.spec;
                 dactyl.clipboardWrite(url, true);
             });
 
@@ -1907,7 +2053,7 @@ var Buffer = Module("Buffer", {
             function (args) { buffer.scrollVertical("lines", -Math.max(args.count, 1)); },
             { count: true });
 
-        mappings.add([modes.COMMAND], dactyl.has("mail") ? ["h", "<scroll-left-column>"] : ["h", "<Left>", "<scroll-left-column>"],
+        mappings.add([modes.NORMAL], dactyl.has("mail") ? ["h", "<scroll-left-column>"] : ["h", "<Left>", "<scroll-left-column>"],
             "Scroll document to the left",
             function (args) { buffer.scrollHorizontal("columns", -Math.max(args.count, 1)); },
             { count: true });
@@ -2369,9 +2515,9 @@ Buffer.addPageInfoSection("e", "Search Engines", function (verbose) {
         if (verbose)
             for (let link in engines)
                 yield [link.title || /*L*/ "Engine " + n++,
-                       <a xmlns={XHTML} href={link.href}
-                          onclick="if (event.button == 0) { window.external.AddSearchProvider(this.href); return false; }"
-                          highlight="URL">{link.href}</a>];
+                       ["a", { href: link.href, highlight: "URL",
+                               onclick: "if (event.button == 0) { window.external.AddSearchProvider(this.href); return false; }" },
+                            link.href]];
     }
 
     if (!verbose && nEngines)
@@ -2427,7 +2573,8 @@ Buffer.addPageInfoSection("f", "Feeds", function (verbose) {
                 nFeed++;
                 let type = feedTypes[feed.type] || "RSS";
                 if (verbose)
-                    yield [feed.title, template.highlightURL(feed.href, true) + <span class="extra-info">&#xa0;({type})</span>];
+                    yield [feed.title, [template.highlightURL(feed.href, true),
+                                        ["span", { class: "extra-info" }, " (" + type + ")"]]];
             }
         }
 
@@ -2477,6 +2624,10 @@ Buffer.addPageInfoSection("g", "General Info", function (verbose) {
     yield ["Title", doc.title];
     yield ["URL", template.highlightURL(doc.location.href, true)];
 
+    let { shortURL } = this;
+    if (shortURL)
+        yield ["Short URL", template.highlightURL(shortURL, true)];
+
     let ref = "referrer" in doc && doc.referrer;
     if (ref)
         yield ["Referrer", template.highlightURL(ref, true)];
@@ -2499,7 +2650,8 @@ Buffer.addPageInfoSection("m", "Meta Tags", function (verbose) {
     // get meta tag data, sort and put into pageMeta[]
     let metaNodes = this.focusedFrame.document.getElementsByTagName("meta");
 
-    return Array.map(metaNodes, function (node) [(node.name || node.httpEquiv), template.highlightURL(node.content)])
+    return Array.map(metaNodes, function (node) [(node.name || node.httpEquiv),
+                                                 template.highlightURL(node.content)])
                 .sort(function (a, b) util.compareIgnoreCase(a[0], b[0]));
 });
 
@@ -2541,7 +2693,7 @@ Buffer.addPageInfoSection("s", "Security", function (verbose) {
     }
 });
 
-} catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
+// catch(e){ if (!e.stack) e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
 
 endModule();