<commit_before>// Copyright (c) 2006-2009 by <NAME> <<EMAIL>>
// Copyright (c) 2007-2009 by <NAME> <<EMAIL>>
// Copyright (c) 2008-2009 by <NAME> <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.


/** @scope modules */

Components.utils.import("resource://gre/modules/XPCOMUtils.jsm", modules);

const Point = Struct("x", "y");

/**
 * A class to manage the primary web content buffer. The name comes
 * from Vim's term, 'buffer', which signifies instances of open
 * files.
 * @instance buffer
 */
const Buffer = Module("buffer", {
    requires: ["config"],

    init: function () {
        this.pageInfo = {};

        this.addPageInfoSection("f", "Feeds", function (verbose) {
            let doc = window.content.document;

            const feedTypes = {
                "application/rss+xml": "RSS",
                "application/atom+xml": "Atom",
                "text/xml": "XML",
                "application/xml": "XML",
                "application/rdf+xml": "XML"
            };

            function isValidFeed(data, principal, isFeed) {
                if (!data || !principal)
                    return false;

                if (!isFeed) {
                    var type = data.type && data.type.toLowerCase();
                    type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");

                    isFeed = ["application/rss+xml", "application/atom+xml"].indexOf(type) >= 0 ||
                             // really slimy: general XML types with magic letters in the title
                             type in feedTypes && /\brss\b/i.test(data.title);
                }

                if (isFeed) {
                    try {
                        window.urlSecurityCheck(data.href, principal,
                                Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
                    }
                    catch (e) {
                        isFeed = false;
                    }
                }

                if (type)
                    data.type = type;

                return isFeed;
            }

            let nFeed = 0;
            for (let link in util.evaluateXPath(["link[@href and (@rel='feed' or (@rel='alternate' and @type))]"], doc)) {
                let rel = link.rel.toLowerCase();
                let feed = { title: link.title, href: link.href, type: link.type || "" };
                if (isValidFeed(feed, doc.nodePrincipal, rel == "feed")) {
                    nFeed++;
                    let type = feedTypes[feed.type] || "RSS";
                    if (verbose)
                        yield [feed.title, template.highlightURL(feed.href, true) + <span class="extra-info">&#xa0;({type})</span>];
                }
            }

            if (!verbose && nFeed)
                yield nFeed + " feed" + (nFeed > 1 ? "s" : "");
        });

        this.addPageInfoSection("g", "General Info", function (verbose) {
            let doc = window.content.document;

            // get file size
            const ACCESS_READ = Ci.nsICache.ACCESS_READ;
            let cacheKey = doc.location.toString().replace(/#.*$/, "");

            for (let proto in util.Array.itervalues(["HTTP", "FTP"])) {
                try {
                    var cacheEntryDescriptor = services.get("cache").createSession(proto, 0, true)
                                                       .openCacheEntry(cacheKey, ACCESS_READ, false);
                    break;
                }
                catch (e) {}
            }

            let pageSize = []; // [0] bytes; [1] kbytes
            if (cacheEntryDescriptor) {
                pageSize[0] = util.formatBytes(cacheEntryDescriptor.dataSize, 0, false);
                pageSize[1] = util.formatBytes(cacheEntryDescriptor.dataSize, 2, true);
                if (pageSize[1] == pageSize[0])
                    pageSize.length = 1; // don't output "xx Bytes" twice
            }

            let lastModVerbose = new Date(doc.lastModified).toLocaleString();
            let lastMod = new Date(doc.lastModified).toLocaleFormat("%x %X");

            if (lastModVerbose == "Invalid Date" || new Date(doc.lastModified).getFullYear() == 1970)
                lastModVerbose = lastMod = null;

            if (!verbose) {
                if (pageSize[0])
                    yield (pageSize[1] || pageSize[0]) + " bytes";
                yield lastMod;
                return;
            }

            yield ["Title", doc.title];
            yield ["URL", template.highlightURL(doc.location.toString(), true)];

            let ref = "referrer" in doc && doc.referrer;
            if (ref)
                yield ["Referrer", template.highlightURL(ref, true)];

            if (pageSize[0])
                yield ["File Size", pageSize[1] ? pageSize[1] + " (" + pageSize[0] + ")"
                                                : pageSize[0]];

            yield ["Mime-Type", doc.contentType];
            yield ["Encoding", doc.characterSet];
            yield ["Compatibility", doc.compatMode == "BackCompat" ? "Quirks Mode" : "Full/Almost Standards Mode"];
            if (lastModVerbose)
                yield ["Last Modified", lastModVerbose];
        });

        this.addPageInfoSection("m", "Meta Tags", function (verbose) {
            // get meta tag data, sort and put into pageMeta[]
            let metaNodes = window.content.document.getElementsByTagName("meta");

            return Array.map(metaNodes, function (node) [(node.name || node.httpEquiv), template.highlightURL(node.content)])
                        .sort(function (a, b) util.compareIgnoreCase(a[0], b[0]));
        });
    },

    destroy: function () {
        try {
            config.browser.removeProgressListener(this.progressListener);
        }
        catch (e) {} // Why? --djk
    },

    _triggerLoadAutocmd: function _triggerLoadAutocmd(name, doc) {
        let args = {
            url:   doc.location.href,
            title: doc.title
        };

        if (liberator.has("tabs")) {
            args.tab = tabs.getContentIndex(doc) + 1;
            args.doc = "tabs.getTab(" + (args.tab - 1) + ").linkedBrowser.contentDocument";
        }

        autocommands.trigger(name, args);
    },

    // called when the active document is scrolled
    _updateBufferPosition: function _updateBufferPosition() {
        statusline.updateBufferPosition();
        modes.show();
    },

    onDOMContentLoaded: function onDOMContentLoaded(event) {
        let doc = event.originalTarget;
        if (doc instanceof HTMLDocument && !doc.defaultView.frameElement)
            this._triggerLoadAutocmd("DOMLoad", doc);
    },

    // TODO: see what can be moved to onDOMContentLoaded()
    // event listener which is is called on each page load, even if the
    // page is loaded in a background tab
    onPageLoad: function onPageLoad(event) {
        if (event.originalTarget instanceof HTMLDocument) {
            let doc = event.originalTarget;
            // document is part of a frameset
            if (doc.defaultView.frameElement) {
                // hacky way to get rid of "Transfering data from ..." on sites with frames
                // when you click on a link inside a frameset, because asyncUpdateUI
                // is not triggered there (Gecko bug?)
                setTimeout(function () { statusline.updateUrl(); }, 10);
                return;
            }

            // code which should happen for all (also background) newly loaded tabs goes here:

            // mark the buffer as loaded, we can't use buffer.loaded
            // since that always refers to the current buffer, while doc can be
            // any buffer, even in a background tab
            doc.pageIsFullyLoaded = 1;

            // code which is only relevant if the page load is the current tab goes here:
            if (doc == config.browser.contentDocument) {
                // we want to stay in command mode after a page has loaded
                // TODO: move somewhere else, as focusing can already happen earlier than on "load"
                if (options["focuscontent"]) {
                    setTimeout(function () {
                        let focused = liberator.focus;
                        if (focused && (focused.value != null) && focused.value.length == 0)
                            focused.blur();
                    }, 0);
                }
            }
            else // background tab
                liberator.echomsg("Background tab loaded: " + doc.title || doc.location.href, 3);

            this._triggerLoadAutocmd("PageLoad", doc);
        }
    },

    /**
     * @property {Object} The document loading progress listener.
     */
    progressListener: {
        QueryInterface: XPCOMUtils.generateQI([
            Ci.nsIWebProgressListener,
            Ci.nsIXULBrowserWindow
        ]),

        // XXX: function may later be needed to detect a canceled synchronous openURL()
        onStateChange: function (webProgress, request, flags, status) {
            // STATE_IS_DOCUMENT | STATE_IS_WINDOW is important, because we also
            // receive statechange events for loading images and other parts of the web page
            if (flags & (Ci.nsIWebProgressListener.STATE_IS_DOCUMENT | Ci.nsIWebProgressListener.STATE_IS_WINDOW)) {
                // This fires when the load event is initiated
                // only thrown for the current tab, not when another tab changes
                if (flags & Ci.nsIWebProgressListener.STATE_START) {
                    buffer.loaded = 0;
                    statusline.updateProgress(0);

                    autocommands.trigger("PageLoadPre", { url: buffer.URL });

                    // don't reset mode if a frame of the frameset gets reloaded which
                    // is not the focused frame
                    if (document.commandDispatcher.focusedWindow == webProgress.DOMWindow) {
                        setTimeout(function () { modes.reset(false); },
                            liberator.mode == modes.HINTS ? 500 : 0);
                    }
                }
                else if (flags & Ci.nsIWebProgressListener.STATE_STOP) {
                    buffer.loaded = (status == 0 ? 1 : 2);
                    statusline.updateUrl();
                }
            }
        },
        // for notifying the user about secure web pages
        onSecurityChange: function (webProgress, request, state) {
            // TODO: do something useful with STATE_SECURE_MED and STATE_SECURE_LOW
            if (state & Ci.nsIWebProgressListener.STATE_IS_INSECURE)
                statusline.setClass("insecure");
            else if (state & Ci.nsIWebProgressListener.STATE_IS_BROKEN)
                statusline.setClass("broken");
            else if (state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL)
                statusline.setClass("extended");
            else if (state & Ci.nsIWebProgressListener.STATE_SECURE_HIGH)
                statusline.setClass("secure");
        },
        onStatusChange: function (webProgress, request, status, message) {
            statusline.updateUrl(message);
        },
        onProgressChange: function (webProgress, request, curSelfProgress, maxSelfProgress, curTotalProgress, maxTotalProgress) {
            statusline.updateProgress(curTotalProgress/maxTotalProgress);
        },
        // happens when the users switches tabs
        onLocationChange: function () {
            statusline.updateUrl();
            statusline.updateProgress();

            autocommands.trigger("LocationChange", { url: buffer.URL });

            // if this is not delayed we get the position of the old buffer
            setTimeout(function () { statusline.updateBufferPosition(); }, 500);
        },
        // called at the very end of a page load
        asyncUpdateUI: function () {
            setTimeout(function () { statusline.updateUrl(); }, 100);
        },
        setOverLink: function (link, b) {
            let ssli = options["showstatuslinks"];
            if (link && ssli) {
                if (ssli == 1)
                    statusline.updateUrl("Link: " + link);
                else if (ssli == 2)
                    liberator.echo("Link: " + link, commandline.DISALLOW_MULTILINE);
            }

            if (link == "") {
                if (ssli == 1)
                    statusline.updateUrl();
                else if (ssli == 2)
                    modes.show();
            }
        },

        // nsIXULBrowserWindow stubs
        setJSDefaultStatus: function (status) {},
        setJSStatus: function (status) {},

        // Stub for something else, presumably. Not in any documented
        // interface.
        onLinkIconAvailable: function () {}
    },

    /**
     * @property {Array} The alternative style sheets for the current
     *     buffer. Only returns style sheets for the 'screen' media type.
     */
    get alternateStyleSheets() {
        let stylesheets = window.getAllStyleSheets(window.content);

        return stylesheets.filter(
            function (stylesheet) /^(screen|all|)$/i.test(stylesheet.media.mediaText) && !/^\s*$/.test(stylesheet.title)
        );
    },

    /**
     * @property {Array[Window]} All frames in the current buffer.
     */
    get allFrames() {
        let frames = [];
        (function (win) {
            frames.push(win);
            Array.slice(win.frames).forEach(arguments.callee);
        })(window.content);
        return frames;
    },

    /**
     * @property {Object} A map of page info sections to their
     *     content generating functions.
     */
    pageInfo: null,

    /**
     * @property {number} A value indicating whether the buffer is loaded.
     *     Values may be:
     *         0 - Loading.
     *         1 - Fully loaded.
     *         2 - Load failed.
     */
    get loaded() {
        if (window.content.document.pageIsFullyLoaded !== undefined)
            return window.content.document.pageIsFullyLoaded;
        return 0; // in doubt return "loading"
    },
    set loaded(value) {
        window.content.document.pageIsFullyLoaded = value;
    },

    /**
     * @property {Object} The local state store for the currently selected
     *     tab.
     */
    get localStore() {
        if (!content.liberatorStore)
            content.liberatorStore = {};
        return content.liberatorStore;
    },



    /**
     * @property {Node} The last focused input field in the buffer. Used
     *     by the "gi" key binding.
     */
    get lastInputField() window.content.document.lastInputField || null,
    set lastInputField(value) { window.content.document.lastInputField = value; },

    /**
     * @property {string} The current top-level document's URL.
     */
    get URL() window.content ? window.content.location.href : config.browser.currentURI.spec,

    /**
     * @property {string} The current top-level document's URL, sans any
     *     fragment identifier.
     */
    get URI() {
        let loc = config.browser.contentWindow.location;
        return loc.href.substr(0, loc.href.length - loc.hash.length);
    },

    /**
     * @property {number} The buffer's height in pixels.
     */
    get pageHeight() window.content.innerHeight,

    /**
     * @property {number} The current browser's text zoom level, as a
     *     percentage with 100 as 'normal'. Only affects text size.
     */
    get textZoom() config.browser.markupDocumentViewer.textZoom * 100,
    set textZoom(value) { Buffer.setZoom(value, false); },

    /**
     * @property {number} The current browser's text zoom level, as a
     *     percentage with 100 as 'normal'. Affects text size, as well as
     *     image size and block size.
     */
    get fullZoom() config.browser.markupDocumentViewer.fullZoom * 100,
    set fullZoom(value) { Buffer.setZoom(value, true); },

    /**
     * @property {string} The current document's title.
     */
    get title() window.content.document.title,

    /**
     * @property {number} The buffer's horizontal scroll percentile.
     */
    get scrollXPercent() {
        let win = Buffer.findScrollableWindow();
        if (win.scrollMaxX > 0)
            return Math.round(win.scrollX / win.scrollMaxX * 100);
        else
            return 0;
    },

    /**
     * @property {number} The buffer's vertical scroll percentile.
     */
    get scrollYPercent() {
        let win = Buffer.findScrollableWindow();
        if (win.scrollMaxY > 0)
            return Math.round(win.scrollY / win.scrollMaxY * 100);
        else
            return 0;
    },

    /**
     * Adds a new section to the page information output.
     *
     * @param {string} option The section's value in 'pageinfo'.
     * @param {string} title The heading for this section's
     *     output.
     * @param {function} func The function to generate this
     *     section's output.
     */
    addPageInfoSection: function addPageInfoSection(option, title, func) {
        this.pageInfo[option] = [func, title];
    },

    /**
     * Returns the currently selected word. If the selection is
     * null, it tries to guess the word that the caret is
     * positioned in.
     *
     * NOTE: might change the selection
     *
     * @returns {string}
     */
    // FIXME: getSelection() doesn't always preserve line endings, see:
    // https://www.mozdev.org/bugs/show_bug.cgi?id=19303
    getCurrentWord: function () {
        function _getCurrentWord (win) {
            let elem = win.frameElement;
            if (elem && elem.getClientRects().length === 0)
                return;
            let selection = win.getSelection();
            if (selection.rangeCount <= 0)
                return;
            let range = selection.getRangeAt(0);
            if (selection.isCollapsed) {
                let selController = buffer.selectionController;
                let caretmode = selController.getCaretEnabled();
                selController.setCaretEnabled(true);
                // Only move backwards if the previous character is not a space.
                if (range.startOffset > 0 && !/\s/.test(range.startContainer.textContent[range.startOffset - 1]))
                    selController.wordMove(false, false);

                selController.wordMove(true, true);
                selController.setCaretEnabled(caretmode);
                return String.match(selection, /\w*/)[0];
            }
            if (util.computedStyle(range.startContainer).whiteSpace == "pre"
                && util.computedStyle(range.endContainer).whiteSpace == "pre")
                return String(range);
            return String(selection);
        }

        return util.Array.compact(buffer.allFrames.map(_getCurrentWord)).join("\n");
    },

    /**
     * Focuses the given element. In contrast to a simple
     * elem.focus() call, this function works for iframes and
     * image maps.
     *
     * @param {Node} elem The element to focus.
     */
    focusElement: function (elem) {
        let doc = window.content.document;
        if (elem instanceof HTMLFrameElement || elem instanceof HTMLIFrameElement)
            Buffer.focusedWindow = elem.contentWindow;
        else if (elem instanceof HTMLInputElement && elem.type == "file") {
            Buffer.openUploadPrompt(elem);
            buffer.lastInputField = elem;
        }
        else {
            elem.focus();

            // for imagemap
            if (elem instanceof HTMLAreaElement) {
                try {
                    let [x, y] = elem.getAttribute("coords").split(",").map(parseFloat);

                    elem.dispatchEvent(events.create(doc, "mouseover", { screenX: x, screenY: y }));
                }
                catch (e) {}
            }
        }
    },

    /**
     * Tries to guess links the like of "next" and "prev". Though it has a
     * singularly horrendous name, it turns out to be quite useful.
     *
     * @param {string} rel The relationship to look for. Looks for
     *     links with matching @rel or @rev attributes, and,
     *     failing that, looks for an option named rel +
     *     "pattern", and finds the last link matching that
     *     RegExp.
     */
    followDocumentRelationship: function (rel) {
        let regexes = options.get(rel + "pattern").values
                             .map(function (re) RegExp(re, "i"));

        function followFrame(frame) {
            function iter(elems) {
                for (let i = 0; i < elems.length; i++)
                    if (elems[i].rel.toLowerCase() == rel || elems[i].rev.toLowerCase() == rel)
                        yield elems[i];
            }

            // <link>s have higher priority than normal <a> hrefs
            let elems = frame.document.getElementsByTagName("link");
            for (let elem in iter(elems)) {
                liberator.open(elem.href);
                return true;
            }

            // no links? ok, look for hrefs
            elems = frame.document.getElementsByTagName("a");
            for (let elem in iter(elems)) {
                buffer.followLink(elem, liberator.CURRENT_TAB);
                return true;
            }

            let res = util.evaluateXPath(options.get("hinttags").defaultValue, frame.document);
            for (let [, regex] in Iterator(regexes)) {
                for (let i in util.range(res.snapshotLength, 0, -1)) {<commit_msg>followDocumentRelationship: Use hinttags value, not defaultValue.<commit_after>// Copyright (c) 2006-2009 by <NAME> <<EMAIL>>
// Copyright (c) 2007-2009 by <NAME> <<EMAIL>>
// Copyright (c) 2008-2009 by <NAME> <<EMAIL> G<EMAIL>>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the License.txt file included with this file.


/** @scope modules */

Components.utils.import("resource://gre/modules/XPCOMUtils.jsm", modules);

const Point = Struct("x", "y");

/**
 * A class to manage the primary web content buffer. The name comes
 * from Vim's term, 'buffer', which signifies instances of open
 * files.
 * @instance buffer
 */
const Buffer = Module("buffer", {
    requires: ["config"],

    init: function () {
        this.pageInfo = {};

        this.addPageInfoSection("f", "Feeds", function (verbose) {
            let doc = window.content.document;

            const feedTypes = {
                "application/rss+xml": "RSS",
                "application/atom+xml": "Atom",
                "text/xml": "XML",
                "application/xml": "XML",
                "application/rdf+xml": "XML"
            };

            function isValidFeed(data, principal, isFeed) {
                if (!data || !principal)
                    return false;

                if (!isFeed) {
                    var type = data.type && data.type.toLowerCase();
                    type = type.replace(/^\s+|\s*(?:;.*)?$/g, "");

                    isFeed = ["application/rss+xml", "application/atom+xml"].indexOf(type) >= 0 ||
                             // really slimy: general XML types with magic letters in the title
                             type in feedTypes && /\brss\b/i.test(data.title);
                }

                if (isFeed) {
                    try {
                        window.urlSecurityCheck(data.href, principal,
                                Ci.nsIScriptSecurityManager.DISALLOW_INHERIT_PRINCIPAL);
                    }
                    catch (e) {
                        isFeed = false;
                    }
                }

                if (type)
                    data.type = type;

                return isFeed;
            }

            let nFeed = 0;
            for (let link in util.evaluateXPath(["link[@href and (@rel='feed' or (@rel='alternate' and @type))]"], doc)) {
                let rel = link.rel.toLowerCase();
                let feed = { title: link.title, href: link.href, type: link.type || "" };
                if (isValidFeed(feed, doc.nodePrincipal, rel == "feed")) {
                    nFeed++;
                    let type = feedTypes[feed.type] || "RSS";
                    if (verbose)
                        yield [feed.title, template.highlightURL(feed.href, true) + <span class="extra-info">&#xa0;({type})</span>];
                }
            }

            if (!verbose && nFeed)
                yield nFeed + " feed" + (nFeed > 1 ? "s" : "");
        });

        this.addPageInfoSection("g", "General Info", function (verbose) {
            let doc = window.content.document;

            // get file size
            const ACCESS_READ = Ci.nsICache.ACCESS_READ;
            let cacheKey = doc.location.toString().replace(/#.*$/, "");

            for (let proto in util.Array.itervalues(["HTTP", "FTP"])) {
                try {
                    var cacheEntryDescriptor = services.get("cache").createSession(proto, 0, true)
                                                       .openCacheEntry(cacheKey, ACCESS_READ, false);
                    break;
                }
                catch (e) {}
            }

            let pageSize = []; // [0] bytes; [1] kbytes
            if (cacheEntryDescriptor) {
                pageSize[0] = util.formatBytes(cacheEntryDescriptor.dataSize, 0, false);
                pageSize[1] = util.formatBytes(cacheEntryDescriptor.dataSize, 2, true);
                if (pageSize[1] == pageSize[0])
                    pageSize.length = 1; // don't output "xx Bytes" twice
            }

            let lastModVerbose = new Date(doc.lastModified).toLocaleString();
            let lastMod = new Date(doc.lastModified).toLocaleFormat("%x %X");

            if (lastModVerbose == "Invalid Date" || new Date(doc.lastModified).getFullYear() == 1970)
                lastModVerbose = lastMod = null;

            if (!verbose) {
                if (pageSize[0])
                    yield (pageSize[1] || pageSize[0]) + " bytes";
                yield lastMod;
                return;
            }

            yield ["Title", doc.title];
            yield ["URL", template.highlightURL(doc.location.toString(), true)];

            let ref = "referrer" in doc && doc.referrer;
            if (ref)
                yield ["Referrer", template.highlightURL(ref, true)];

            if (pageSize[0])
                yield ["File Size", pageSize[1] ? pageSize[1] + " (" + pageSize[0] + ")"
                                                : pageSize[0]];

            yield ["Mime-Type", doc.contentType];
            yield ["Encoding", doc.characterSet];
            yield ["Compatibility", doc.compatMode == "BackCompat" ? "Quirks Mode" : "Full/Almost Standards Mode"];
            if (lastModVerbose)
                yield ["Last Modified", lastModVerbose];
        });

        this.addPageInfoSection("m", "Meta Tags", function (verbose) {
            // get meta tag data, sort and put into pageMeta[]
            let metaNodes = window.content.document.getElementsByTagName("meta");

            return Array.map(metaNodes, function (node) [(node.name || node.httpEquiv), template.highlightURL(node.content)])
                        .sort(function (a, b) util.compareIgnoreCase(a[0], b[0]));
        });
    },

    destroy: function () {
        try {
            config.browser.removeProgressListener(this.progressListener);
        }
        catch (e) {} // Why? --djk
    },

    _triggerLoadAutocmd: function _triggerLoadAutocmd(name, doc) {
        let args = {
            url:   doc.location.href,
            title: doc.title
        };

        if (liberator.has("tabs")) {
            args.tab = tabs.getContentIndex(doc) + 1;
            args.doc = "tabs.getTab(" + (args.tab - 1) + ").linkedBrowser.contentDocument";
        }

        autocommands.trigger(name, args);
    },

    // called when the active document is scrolled
    _updateBufferPosition: function _updateBufferPosition() {
        statusline.updateBufferPosition();
        modes.show();
    },

    onDOMContentLoaded: function onDOMContentLoaded(event) {
        let doc = event.originalTarget;
        if (doc instanceof HTMLDocument && !doc.defaultView.frameElement)
            this._triggerLoadAutocmd("DOMLoad", doc);
    },

    // TODO: see what can be moved to onDOMContentLoaded()
    // event listener which is is called on each page load, even if the
    // page is loaded in a background tab
    onPageLoad: function onPageLoad(event) {
        if (event.originalTarget instanceof HTMLDocument) {
            let doc = event.originalTarget;
            // document is part of a frameset
            if (doc.defaultView.frameElement) {
                // hacky way to get rid of "Transfering data from ..." on sites with frames
                // when you click on a link inside a frameset, because asyncUpdateUI
                // is not triggered there (Gecko bug?)
                setTimeout(function () { statusline.updateUrl(); }, 10);
                return;
            }

            // code which should happen for all (also background) newly loaded tabs goes here:

            // mark the buffer as loaded, we can't use buffer.loaded
            // since that always refers to the current buffer, while doc can be
            // any buffer, even in a background tab
            doc.pageIsFullyLoaded = 1;

            // code which is only relevant if the page load is the current tab goes here:
            if (doc == config.browser.contentDocument) {
                // we want to stay in command mode after a page has loaded
                // TODO: move somewhere else, as focusing can already happen earlier than on "load"
                if (options["focuscontent"]) {
                    setTimeout(function () {
                        let focused = liberator.focus;
                        if (focused && (focused.value != null) && focused.value.length == 0)
                            focused.blur();
                    }, 0);
                }
            }
            else // background tab
                liberator.echomsg("Background tab loaded: " + doc.title || doc.location.href, 3);

            this._triggerLoadAutocmd("PageLoad", doc);
        }
    },

    /**
     * @property {Object} The document loading progress listener.
     */
    progressListener: {
        QueryInterface: XPCOMUtils.generateQI([
            Ci.nsIWebProgressListener,
            Ci.nsIXULBrowserWindow
        ]),

        // XXX: function may later be needed to detect a canceled synchronous openURL()
        onStateChange: function (webProgress, request, flags, status) {
            // STATE_IS_DOCUMENT | STATE_IS_WINDOW is important, because we also
            // receive statechange events for loading images and other parts of the web page
            if (flags & (Ci.nsIWebProgressListener.STATE_IS_DOCUMENT | Ci.nsIWebProgressListener.STATE_IS_WINDOW)) {
                // This fires when the load event is initiated
                // only thrown for the current tab, not when another tab changes
                if (flags & Ci.nsIWebProgressListener.STATE_START) {
                    buffer.loaded = 0;
                    statusline.updateProgress(0);

                    autocommands.trigger("PageLoadPre", { url: buffer.URL });

                    // don't reset mode if a frame of the frameset gets reloaded which
                    // is not the focused frame
                    if (document.commandDispatcher.focusedWindow == webProgress.DOMWindow) {
                        setTimeout(function () { modes.reset(false); },
                            liberator.mode == modes.HINTS ? 500 : 0);
                    }
                }
                else if (flags & Ci.nsIWebProgressListener.STATE_STOP) {
                    buffer.loaded = (status == 0 ? 1 : 2);
                    statusline.updateUrl();
                }
            }
        },
        // for notifying the user about secure web pages
        onSecurityChange: function (webProgress, request, state) {
            // TODO: do something useful with STATE_SECURE_MED and STATE_SECURE_LOW
            if (state & Ci.nsIWebProgressListener.STATE_IS_INSECURE)
                statusline.setClass("insecure");
            else if (state & Ci.nsIWebProgressListener.STATE_IS_BROKEN)
                statusline.setClass("broken");
            else if (state & Ci.nsIWebProgressListener.STATE_IDENTITY_EV_TOPLEVEL)
                statusline.setClass("extended");
            else if (state & Ci.nsIWebProgressListener.STATE_SECURE_HIGH)
                statusline.setClass("secure");
        },
        onStatusChange: function (webProgress, request, status, message) {
            statusline.updateUrl(message);
        },
        onProgressChange: function (webProgress, request, curSelfProgress, maxSelfProgress, curTotalProgress, maxTotalProgress) {
            statusline.updateProgress(curTotalProgress/maxTotalProgress);
        },
        // happens when the users switches tabs
        onLocationChange: function () {
            statusline.updateUrl();
            statusline.updateProgress();

            autocommands.trigger("LocationChange", { url: buffer.URL });

            // if this is not delayed we get the position of the old buffer
            setTimeout(function () { statusline.updateBufferPosition(); }, 500);
        },
        // called at the very end of a page load
        asyncUpdateUI: function () {
            setTimeout(function () { statusline.updateUrl(); }, 100);
        },
        setOverLink: function (link, b) {
            let ssli = options["showstatuslinks"];
            if (link && ssli) {
                if (ssli == 1)
                    statusline.updateUrl("Link: " + link);
                else if (ssli == 2)
                    liberator.echo("Link: " + link, commandline.DISALLOW_MULTILINE);
            }

            if (link == "") {
                if (ssli == 1)
                    statusline.updateUrl();
                else if (ssli == 2)
                    modes.show();
            }
        },

        // nsIXULBrowserWindow stubs
        setJSDefaultStatus: function (status) {},
        setJSStatus: function (status) {},

        // Stub for something else, presumably. Not in any documented
        // interface.
        onLinkIconAvailable: function () {}
    },

    /**
     * @property {Array} The alternative style sheets for the current
     *     buffer. Only returns style sheets for the 'screen' media type.
     */
    get alternateStyleSheets() {
        let stylesheets = window.getAllStyleSheets(window.content);

        return stylesheets.filter(
            function (stylesheet) /^(screen|all|)$/i.test(stylesheet.media.mediaText) && !/^\s*$/.test(stylesheet.title)
        );
    },

    /**
     * @property {Array[Window]} All frames in the current buffer.
     */
    get allFrames() {
        let frames = [];
        (function (win) {
            frames.push(win);
            Array.slice(win.frames).forEach(arguments.callee);
        })(window.content);
        return frames;
    },

    /**
     * @property {Object} A map of page info sections to their
     *     content generating functions.
     */
    pageInfo: null,

    /**
     * @property {number} A value indicating whether the buffer is loaded.
     *     Values may be:
     *         0 - Loading.
     *         1 - Fully loaded.
     *         2 - Load failed.
     */
    get loaded() {
        if (window.content.document.pageIsFullyLoaded !== undefined)
            return window.content.document.pageIsFullyLoaded;
        return 0; // in doubt return "loading"
    },
    set loaded(value) {
        window.content.document.pageIsFullyLoaded = value;
    },

    /**
     * @property {Object} The local state store for the currently selected
     *     tab.
     */
    get localStore() {
        if (!content.liberatorStore)
            content.liberatorStore = {};
        return content.liberatorStore;
    },



    /**
     * @property {Node} The last focused input field in the buffer. Used
     *     by the "gi" key binding.
     */
    get lastInputField() window.content.document.lastInputField || null,
    set lastInputField(value) { window.content.document.lastInputField = value; },

    /**
     * @property {string} The current top-level document's URL.
     */
    get URL() window.content ? window.content.location.href : config.browser.currentURI.spec,

    /**
     * @property {string} The current top-level document's URL, sans any
     *     fragment identifier.
     */
    get URI() {
        let loc = config.browser.contentWindow.location;
        return loc.href.substr(0, loc.href.length - loc.hash.length);
    },

    /**
     * @property {number} The buffer's height in pixels.
     */
    get pageHeight() window.content.innerHeight,

    /**
     * @property {number} The current browser's text zoom level, as a
     *     percentage with 100 as 'normal'. Only affects text size.
     */
    get textZoom() config.browser.markupDocumentViewer.textZoom * 100,
    set textZoom(value) { Buffer.setZoom(value, false); },

    /**
     * @property {number} The current browser's text zoom level, as a
     *     percentage with 100 as 'normal'. Affects text size, as well as
     *     image size and block size.
     */
    get fullZoom() config.browser.markupDocumentViewer.fullZoom * 100,
    set fullZoom(value) { Buffer.setZoom(value, true); },

    /**
     * @property {string} The current document's title.
     */
    get title() window.content.document.title,

    /**
     * @property {number} The buffer's horizontal scroll percentile.
     */
    get scrollXPercent() {
        let win = Buffer.findScrollableWindow();
        if (win.scrollMaxX > 0)
            return Math.round(win.scrollX / win.scrollMaxX * 100);
        else
            return 0;
    },

    /**
     * @property {number} The buffer's vertical scroll percentile.
     */
    get scrollYPercent() {
        let win = Buffer.findScrollableWindow();
        if (win.scrollMaxY > 0)
            return Math.round(win.scrollY / win.scrollMaxY * 100);
        else
            return 0;
    },

    /**
     * Adds a new section to the page information output.
     *
     * @param {string} option The section's value in 'pageinfo'.
     * @param {string} title The heading for this section's
     *     output.
     * @param {function} func The function to generate this
     *     section's output.
     */
    addPageInfoSection: function addPageInfoSection(option, title, func) {
        this.pageInfo[option] = [func, title];
    },

    /**
     * Returns the currently selected word. If the selection is
     * null, it tries to guess the word that the caret is
     * positioned in.
     *
     * NOTE: might change the selection
     *
     * @returns {string}
     */
    // FIXME: getSelection() doesn't always preserve line endings, see:
    // https://www.mozdev.org/bugs/show_bug.cgi?id=19303
    getCurrentWord: function () {
        function _getCurrentWord (win) {
            let elem = win.frameElement;
            if (elem && elem.getClientRects().length === 0)
                return;
            let selection = win.getSelection();
            if (selection.rangeCount <= 0)
                return;
            let range = selection.getRangeAt(0);
            if (selection.isCollapsed) {
                let selController = buffer.selectionController;
                let caretmode = selController.getCaretEnabled();
                selController.setCaretEnabled(true);
                // Only move backwards if the previous character is not a space.
                if (range.startOffset > 0 && !/\s/.test(range.startContainer.textContent[range.startOffset - 1]))
                    selController.wordMove(false, false);

                selController.wordMove(true, true);
                selController.setCaretEnabled(caretmode);
                return String.match(selection, /\w*/)[0];
            }
            if (util.computedStyle(range.startContainer).whiteSpace == "pre"
                && util.computedStyle(range.endContainer).whiteSpace == "pre")
                return String(range);
            return String(selection);
        }

        return util.Array.compact(buffer.allFrames.map(_getCurrentWord)).join("\n");
    },

    /**
     * Focuses the given element. In contrast to a simple
     * elem.focus() call, this function works for iframes and
     * image maps.
     *
     * @param {Node} elem The element to focus.
     */
    focusElement: function (elem) {
        let doc = window.content.document;
        if (elem instanceof HTMLFrameElement || elem instanceof HTMLIFrameElement)
            Buffer.focusedWindow = elem.contentWindow;
        else if (elem instanceof HTMLInputElement && elem.type == "file") {
            Buffer.openUploadPrompt(elem);
            buffer.lastInputField = elem;
        }
        else {
            elem.focus();

            // for imagemap
            if (elem instanceof HTMLAreaElement) {
                try {
                    let [x, y] = elem.getAttribute("coords").split(",").map(parseFloat);

                    elem.dispatchEvent(events.create(doc, "mouseover", { screenX: x, screenY: y }));
                }
                catch (e) {}
            }
        }
    },

    /**
     * Tries to guess links the like of "next" and "prev". Though it has a
     * singularly horrendous name, it turns out to be quite useful.
     *
     * @param {string} rel The relationship to look for. Looks for
     *     links with matching @rel or @rev attributes, and,
     *     failing that, looks for an option named rel +
     *     "pattern", and finds the last link matching that
     *     RegExp.
     */
    followDocumentRelationship: function (rel) {
        let regexes = options.get(rel + "pattern").values
                             .map(function (re) RegExp(re, "i"));

        function followFrame(frame) {
            function iter(elems) {
                for (let i = 0; i < elems.length; i++)
                    if (elems[i].rel.toLowerCase() == rel || elems[i].rev.toLowerCase() == rel)
                        yield elems[i];
            }

            // <link>s have higher priority than normal <a> hrefs
            let elems = frame.document.getElementsByTagName("link");
            for (let elem in iter(elems)) {
                liberator.open(elem.href);
                return true;
            }

            // no links? ok, look for hrefs
            elems = frame.document.getElementsByTagName("a");
            for (let elem in iter(elems)) {
                buffer.followLink(elem, liberator.CURRENT_TAB);
                return true;
            }

            let res = util.evaluateXPath(options.get("hinttags").value, frame.document);
            for (let [, regex] in Iterator(regexes)) {
                for (let i in util.range(res.snapshotLength, 0, -1)) {
{'fraction_non_alphanumeric': 0.11194203167820149, 'fraction_numerical': 0.005573877096010033, 'mean_word_length': 1.7497924516252634, 'pattern_counts': {'":': 10, '<': 33, '<?xml version=': 0, '>': 39, 'https://': 2, 'lorem ipsum': 0, 'www.': 2, 'xml': 14}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 8, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5736396', 'n_tokens_mistral': 11735, 'n_tokens_neox': 11084, 'n_words': 3108}