// Copyright (c) 2008-2014 Kris Maglione <maglione.k@gmail.com>
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
"use strict";

defineModule("finder", {
    exports: ["RangeFind", "RangeFinder", "rangefinder"],
    require: ["prefs", "util"]
});

lazyRequire("buffer", ["Buffer"]);
lazyRequire("overlay", ["overlay"]);

function id(w) {
    return w.QueryInterface(Ci.nsIInterfaceRequestor)
            .getInterface(Ci.nsIDOMWindowUtils)
            .outerWindowID;
}
function equals(a, b) {
    return id(a) == id(b);
}

/** @instance rangefinder */
var RangeFinder = Module("rangefinder", {
    Local: function (dactyl, modules, window) {
        return {
            init: function () {
                this.dactyl = dactyl;
                this.modules = modules;
                this.window = window;
                this.lastFindPattern = "";
            },

            get content() {
                let { window } = this.modes.getStack(0).params;
                return window || this.window.content;
            },

            get rangeFind() {
                let find = overlay.getData(this.content.document,
                                        "range-find", null);

                if (!isinstance(find, RangeFind) || find.stale)
                    return this.rangeFind = null;
                return find;
            },
            set rangeFind(val) {
                overlay.setData(this.content.document,
                                "range-find", val);
            }
        };
    },

    init: function init() {
        prefs.safeSet("accessibility.typeaheadfind.autostart", false);
        // The above should be sufficient, but: http://bugzil.la/348187
        prefs.safeSet("accessibility.typeaheadfind", false);
    },

    cleanup: function cleanup() {
        for (let doc of util.iterDocuments()) {
            let find = overlay.getData(doc, "range-find", null);
            if (find)
                find.highlight(true);

            overlay.setData(doc, "range-find", null);
        }
    },

    get commandline() { return this.modules.commandline; },
    get modes() { return this.modules.modes; },
    get options() { return this.modules.options; },

    openPrompt: function openPrompt(mode) {
        this.modules.marks.push();
        this.commandline;
        this.CommandMode(mode, this.content).open();

        Buffer(this.content).resetCaret();

        if (this.rangeFind && equals(this.rangeFind.window.get(), this.window))
            this.rangeFind.reset();
        this.find("", mode == this.modes.FIND_BACKWARD);
    },

    bootstrap: function bootstrap(str, backward=this.rangeFind && this.rangeFind.reverse) {

        let highlighted = this.rangeFind && this.rangeFind.highlighted;
        let selections = this.rangeFind && this.rangeFind.selections;
        let linksOnly = false;
        let regexp = false;
        let matchCase = this.options["findcase"] === "smart"  ? /[A-Z]/.test(str) :
                        this.options["findcase"] === "ignore" ? false : true;

        function replacer(m, n1) {
            if (n1 == "c")
                matchCase = false;
            else if (n1 == "C")
                matchCase = true;
            else if (n1 == "l")
                linksOnly = true;
            else if (n1 == "L")
                linksOnly = false;
            else if (n1 == "r")
                regexp = true;
            else if (n1 == "R")
                regexp = false;
            else
                return m;
            return "";
        }

        this.options["findflags"].forEach(f => replacer(f, f));

        let pattern = str.replace(/\\(.|$)/g, replacer);

        if (str)
            this.lastFindPattern = str;
        // It's possible, with :tabdetach for instance, for the rangeFind to
        // actually move from one window to another, which breaks things.
        if (!this.rangeFind
            || !equals(this.rangeFind.window.get(), this.window)
            || linksOnly  != !!this.rangeFind.elementPath
            || regexp     != this.rangeFind.regexp
            || matchCase  != this.rangeFind.matchCase
            || !!backward != this.rangeFind.reverse) {

            if (this.rangeFind)
                this.rangeFind.cancel();
            this.rangeFind = null;
            this.rangeFind = RangeFind(this.window, this.content, matchCase, backward,
                                       linksOnly && this.options.get("hinttags").matcher,
                                       regexp);
            this.rangeFind.highlighted = highlighted;
            this.rangeFind.selections = selections;
        }
        this.rangeFind.pattern = str;
        return pattern;
    },

    find: function find(pattern, backwards) {
        this.modules.marks.push();
        let str = this.bootstrap(pattern, backwards);
        this.backward = this.rangeFind.reverse;

        if (!this.rangeFind.find(str))
            this.dactyl.echoerr(_("finder.notFound", pattern),
                                this.commandline.FORCE_SINGLELINE);

        return this.rangeFind.found;
    },

    findAgain: function findAgain(reverse) {
        this.modules.marks.push();
        if (!this.rangeFind)
            this.find(this.lastFindPattern);
        else if (!this.rangeFind.find(null, reverse))
            this.dactyl.echoerr(_("finder.notFound", this.lastFindPattern),
                                this.commandline.FORCE_SINGLELINE);
        else if (this.rangeFind.wrapped) {
            let msg = this.rangeFind.backward ? _("finder.atTop")
                                              : _("finder.atBottom");
            this.commandline.echo(msg, "WarningMsg", this.commandline.APPEND_TO_MESSAGES
                                                   | this.commandline.FORCE_SINGLELINE);
        }
        else
            this.commandline.echo((this.rangeFind.backward ? "?" : "/") + this.rangeFind.pattern,
                                  "Normal", this.commandline.FORCE_SINGLELINE);

        if (this.options["hlfind"])
            this.highlight();
        this.rangeFind.focus();
    },

    onCancel: function onCancel() {
        if (this.rangeFind)
            this.rangeFind.cancel();
    },

    onChange: function onChange(command) {
        if (this.options["incfind"]) {
            command = this.bootstrap(command);
            this.rangeFind.find(command);
        }
    },

    onHistory: function onHistory() {
        this.rangeFind.found = false;
    },

    onSubmit: function onSubmit(command) {
        if (!command && this.lastFindPattern) {
            this.find(this.lastFindPattern, this.backward);
            this.findAgain();
            return;
        }

        if (!this.options["incfind"] || !this.rangeFind || !this.rangeFind.found) {
            this.clear();
            this.find(command || this.lastFindPattern, this.backward);
        }

        if (this.options["hlfind"])
            this.highlight();
        this.rangeFind.focus();
    },

    /**
     * Highlights all occurrences of the last sought for string in the
     * current buffer.
     */
    highlight: function highlight() {
        if (this.rangeFind)
            this.rangeFind.highlight();
    },

    /**
     * Clears all find highlighting.
     */
    clear: function clear() {
        if (this.rangeFind)
            this.rangeFind.highlight(true);
    }
}, {
}, {
    modes: function initModes(dactyl, modules, window) {
        initModes.require("commandline");

        const { modes } = modules;

        modes.addMode("FIND", {
            description: "Find mode, active when typing search input",
            bases: [modes.COMMAND_LINE]
        });
        modes.addMode("FIND_FORWARD", {
            description: "Forward Find mode, active when typing search input",
            bases: [modes.FIND]
        });
        modes.addMode("FIND_BACKWARD", {
            description: "Backward Find mode, active when typing search input",
            bases: [modes.FIND]
        });
    },
    commands: function initCommands(dactyl, modules, window) {
        const { commands, rangefinder } = modules;
        commands.add(["noh[lfind]"],
            "Remove the find highlighting",
            function () { rangefinder.clear(); },
            { argCount: "0" });
    },
    commandline: function initCommandline(dactyl, modules, window) {
        const { rangefinder } = modules;
        rangefinder.CommandMode = Class("CommandFindMode", modules.CommandMode, {
            init: function init(mode, window) {
                this.mode = mode;
                this.window = window;
                init.supercall(this);
            },

            historyKey: "find",

            get prompt() {
                return this.mode === modules.modes.FIND_BACKWARD ? "?" : "/";
            },

            get onCancel()  { return modules.rangefinder.bound.onCancel; },
            get onChange()  { return modules.rangefinder.bound.onChange; },
            get onHistory() { return modules.rangefinder.bound.onHistory; },
            get onSubmit()  { return modules.rangefinder.bound.onSubmit; }
        });
    },
    mappings: function initMappings(dactyl, modules, window) {
        const { Buffer, buffer, config, mappings, modes, rangefinder } = modules;
        var myModes = config.browserModes.concat([modes.CARET]);

        mappings.add(myModes,
            ["/", "<find-forward>"], "Find a pattern starting at the current caret position",
            function () { rangefinder.openPrompt(modes.FIND_FORWARD); });

        mappings.add(myModes,
            ["?", "<find-backward>", "<S-Slash>"], "Find a pattern backward of the current caret position",
            function () { rangefinder.openPrompt(modes.FIND_BACKWARD); });

        mappings.add(myModes,
            ["n", "<find-next>"], "Find next",
            function () { rangefinder.findAgain(false); });

        mappings.add(myModes,
            ["N", "<find-previous>"], "Find previous",
            function () { rangefinder.findAgain(true); });

        mappings.add(myModes.concat([modes.CARET, modes.TEXT_EDIT]), ["*", "<find-word-forward>"],
            "Find word under cursor",
            function () {
                rangefinder.find(Buffer.currentWord(buffer.focusedFrame, true), false);
                rangefinder.findAgain();
            });

        mappings.add(myModes.concat([modes.CARET, modes.TEXT_EDIT]), ["#", "<find-word-backward>"],
            "Find word under cursor backwards",
            function () {
                rangefinder.find(Buffer.currentWord(buffer.focusedFrame, true), true);
                rangefinder.findAgain();
            });

    },
    options: function initOptions(dactyl, modules, window) {
        const { options, rangefinder } = modules;

        options.add(["hlfind", "hlf"],
            "Highlight all /find pattern matches on the current page after submission",
            "boolean", false, {
                setter: function (value) {
                    rangefinder[value ? "highlight" : "clear"]();
                    return value;
                }
            });

        options.add(["findcase", "fc"],
            "Find case matching mode",
            "string", "smart",
            {
                values: {
                    "smart": "Case is significant when capital letters are typed",
                    "match": "Case is always significant",
                    "ignore": "Case is never significant"
                }
            });

        options.add(["findflags", "ff"],
            "Default flags for find invocations",
            "charlist", "",
            {
                values: {
                    "c": "Ignore case",
                    "C": "Match case",
                    "r": "Perform a regular expression search",
                    "R": "Perform a plain string search",
                    "l": "Search only in links",
                    "L": "Search all text"
                }
            });

        options.add(["incfind", "if"],
            "Find a pattern incrementally as it is typed rather than awaiting c_<Return>",
            "boolean", true);
    }
});

/**
 * @class RangeFind
 *
 * A fairly sophisticated typeahead-find replacement. It supports
 * incremental find very much as the builtin component.
 * Additionally, it supports several features impossible to
 * implement using the standard component. Incremental finding
 * works both forwards and backwards. Erasing characters during an
 * incremental find moves the selection back to the first
 * available match for the shorter term. The selection and viewport
 * are restored when the find is canceled.
 *
 * Also, in addition to full support for frames and iframes, this
 * implementation will begin finding from the position of the
 * caret in the last active frame. This is contrary to the behavior
 * of the builtin component, which always starts a find from the
 * beginning of the first frame in the case of frameset documents,
 * and cycles through all frames from beginning to end. This makes it
 * impossible to choose the starting point of a find for such
 * documents, and represents a major detriment to productivity where
 * large amounts of data are concerned (e.g., for API documents).
 */
var RangeFind = Class("RangeFind", {
    init: function init(window, content, matchCase, backward, elementPath, regexp) {
        this.window = util.weakReference(window);
        this.content = content;

        this.baseDocument = util.weakReference(this.content.document);
        this.elementPath = elementPath || null;
        this.reverse = Boolean(backward);

        this.finder = services.Find();
        this.matchCase = Boolean(matchCase);
        this.regexp = Boolean(regexp);

        this.reset();

        this.highlighted = null;
        this.selections = [];
        this.lastString = "";
    },

    get store() {
        return overlay.getData(this.content.document, "buffer", Object);
    },

    get backward() { return this.finder.findBackwards; },
    set backward(val) { this.finder.findBackwards = val; },

    get matchCase() { return this.finder.caseSensitive; },
    set matchCase(val) { this.finder.caseSensitive = Boolean(val); },

    get findString() { return this.lastString; },

    get flags() { return this.matchCase ? "" : "i"; },

    get selectedRange() {
        let win = this.store.focusedFrame && this.store.focusedFrame.get() || this.content;

        let selection = win.getSelection();
        return (selection.rangeCount ? selection.getRangeAt(0) : this.ranges[0].range).cloneRange();
    },
    set selectedRange(range) {
        this.range.selection.removeAllRanges();
        this.range.selection.addRange(range);
        this.range.selectionController.scrollSelectionIntoView(
            this.range.selectionController.SELECTION_NORMAL, 0, false);

        this.store.focusedFrame = util.weakReference(range.startContainer.ownerDocument.defaultView);
    },

    cancel: function cancel() {
        this.purgeListeners();
        if (this.range) {
            this.range.deselect();
            this.range.descroll();
        }
    },

    compareRanges: function compareRanges(r1, r2) {
        try {
            return this.backward ?  r1.compareBoundaryPoints(r1.END_TO_START, r2)
                                 : -r1.compareBoundaryPoints(r1.START_TO_END, r2);
        }
        catch (e) {
            util.reportError(e);
            return 0;
        }
    },

    findRange: function findRange(range) {
        let doc = range.startContainer.ownerDocument;
        let win = doc.defaultView;
        let ranges = this.ranges.filter(r =>
            r.window === win && RangeFind.sameDocument(r.range, range) && RangeFind.contains(r.range, range));

        if (this.backward)
            return ranges[ranges.length - 1];
        return ranges[0];
    },

    findSubRanges: function* findSubRanges(range) {
        let doc = range.startContainer.ownerDocument;
        for (let elem of this.elementPath(doc)) {
            let r = RangeFind.nodeRange(elem);
            if (RangeFind.contains(range, r))
                yield r;
        }
    },

    focus: function focus() {
        if (this.lastRange)
            var node = DOM.XPath(RangeFind.selectNodePath,
                                 this.lastRange.commonAncestorContainer).snapshotItem(0);
        if (node) {
            node.focus();
            // Re-highlight collapsed selection
            this.selectedRange = this.lastRange;
        }
    },

    highlight: function highlight(clear) {
        if (!clear && (!this.lastString || this.lastString == this.highlighted))
            return;
        if (clear && !this.highlighted)
            return;

        if (!clear && this.highlighted)
            this.highlight(true);

        if (clear) {
            this.selections.forEach(function (selection) {
                selection.removeAllRanges();
            });
            this.selections = [];
            this.highlighted = null;
        }
        else {
            this.selections = [];
            let string = this.lastString;
            for (let r of this.iter(string)) {
                let controller = this.range.selectionController;
                for (let node = r.startContainer; node; node = node.parentNode)
                    if (node instanceof Ci.nsIDOMNSEditableElement) {
                        controller = node.editor.selectionController;
                        break;
                    }

                let sel = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
                sel.addRange(r);
                if (this.selections.indexOf(sel) < 0)
                    this.selections.push(sel);
            }
            this.highlighted = this.lastString;
            if (this.lastRange)
                this.selectedRange = this.lastRange;
            this.addListeners();
        }
    },

    indexIter: function* indexIter(private_) {
        let idx = this.range.index;
        if (this.backward)
            var groups = [util.range(idx + 1, 0, -1), util.range(this.ranges.length, idx, -1)];
        else
            var groups = [util.range(idx, this.ranges.length), util.range(0, idx + 1)];

        for (let i of groups[0])
            yield i;

        if (!private_) {
            this.wrapped = true;
            this.lastRange = null;
            for (let i of groups[1])
                yield i;
        }
    },

    iter: function* iter(word) {
        let saved = ["lastRange", "lastString", "range", "regexp"].map(s => [s, this[s]]);
        let res;
        try {
            let regexp = this.regexp && word != util.regexp.escape(word);
            this.lastRange = null;
            this.regexp = false;
            if (regexp) {
                let re = RegExp(word, "gm" + this.flags);
                for (this.range of this.ranges) {
                    for (let match of util.regexp.iterate(re, DOM.stringify(this.range.range, true))) {
                        let lastRange = this.lastRange;
                        if (res = this.find(null, this.reverse, true))
                            yield res;
                        else
                            this.lastRange = lastRange;
                    }
                }
            }
            else {
                this.range = this.ranges[0];
                this.lastString = word;
                while (res = this.find(null, this.reverse, true))
                    yield res;
            }
        }
        finally {
            saved.forEach(([k, v]) => { this[k] = v; });
        }
    },

    makeFrameList: function makeFrameList(win) {
        const self = this;
        win = win.top;
        let frames = [];
        let backup = null;

        function pushRange(start, end) {
            function push(r) {
                if (r = RangeFind.Range(r, frames.length))
                    frames.push(r);
            }

            let doc = start.startContainer.ownerDocument;

            let range = doc.createRange();
            range.setStart(start.startContainer, start.startOffset);
            range.setEnd(end.startContainer, end.startOffset);

            if (!self.elementPath)
                push(range);
            else
                for (let r of self.findSubRanges(range))
                    push(r);
        }
        function rec(win) {
            let doc = win.document;
            let pageRange = RangeFind[doc.body ? "nodeRange" : "nodeContents"](doc.body || doc.documentElement);
            backup = backup || pageRange;
            let pageStart = RangeFind.endpoint(pageRange, true);
            let pageEnd = RangeFind.endpoint(pageRange, false);

            for (let frame of Ary.iterValues(win.frames)) {
                let range = doc.createRange();
                if (DOM(frame.frameElement).style.visibility == "visible") {
                    range.selectNode(frame.frameElement);
                    pushRange(pageStart, RangeFind.endpoint(range, true));
                    pageStart = RangeFind.endpoint(range, false);
                    rec(frame);
                }
            }
            pushRange(pageStart, pageEnd);

            let anonNodes = doc.getAnonymousNodes(doc.documentElement);
            if (anonNodes) {
                for (let elem of anonNodes) {
                    let range = RangeFind.nodeContents(elem);
                    pushRange(RangeFind.endpoint(range, true), RangeFind.endpoint(range, false));
                }
            }
        }
        rec(win);
        if (frames.length == 0)
            frames[0] = RangeFind.Range(RangeFind.endpoint(backup, true), 0);
        return frames;
    },

    reset: function reset() {
        this.ranges = this.makeFrameList(this.content);

        this.startRange = this.selectedRange;
        this.startRange.collapse(!this.reverse);
        this.lastRange = this.selectedRange;
        this.range = this.findRange(this.startRange) || this.ranges[0];
        util.assert(this.range, "Null range", false);
        this.ranges.first = this.range;
        this.ranges.forEach(range => { range.save(); });
        this.forward = null;
        this.found = false;
    },

    find: function find(pattern, reverse, private_) {
        if (!private_ && this.lastRange && !RangeFind.equal(this.selectedRange, this.lastRange))
            this.reset();

        this.wrapped = false;
        this.backward = reverse ? !this.reverse : this.reverse;
        let again = pattern == null;
        if (again)
            pattern = this.lastString;
        if (!this.matchCase)
            pattern = pattern.toLowerCase();

        if (!again && (pattern === "" || !pattern.startsWith(this.lastString) || this.backward)) {
            if (!private_)
                this.range.deselect();
            if (pattern === "")
                this.range.descroll();
            this.lastRange = this.startRange;
            this.range = this.ranges.first;
        }

        let word = pattern;
        let regexp = this.regexp && word != util.regexp.escape(word);

        if (regexp)
            try {
                RegExp(pattern);
            }
            catch (e) {
                pattern = "";
            }

        if (pattern == "")
            var range = this.startRange;
        else
            for (let i of this.indexIter(private_)) {
                if (!private_ && this.range.window != this.ranges[i].window && this.range.window != this.ranges[i].window.parent) {
                    this.range.descroll();
                    this.range.deselect();
                }
                this.range = this.ranges[i];

                let start = RangeFind.sameDocument(this.lastRange, this.range.range) && this.range.intersects(this.lastRange) ?
                                RangeFind.endpoint(this.lastRange, !(again ^ this.backward)) :
                                RangeFind.endpoint(this.range.range, !this.backward);

                if (this.backward && !again)
                    start = RangeFind.endpoint(this.startRange, false);

                if (regexp) {
                    let range = this.range.range.cloneRange();
                    range[this.backward ? "setEnd" : "setStart"](
                        start.startContainer, start.startOffset);
                    range = DOM.stringify(range);

                    if (!this.backward)
                        var match = RegExp(pattern, "m" + this.flags).exec(range);
                    else {
                        match = RegExp("[^]*(?:" + pattern + ")", "m" + this.flags).exec(range);
                        if (match)
                            match = RegExp(pattern + "$", this.flags).exec(match[0]);
                    }
                    if (!(match && match[0]))
                        continue;
                    word = match[0];
                }

                var range = this.finder.Find(word, this.range.range, start, this.range.range);
                if (range && DOM(range.commonAncestorContainer).isVisible)
                    break;
            }

        if (range)
            this.lastRange = range.cloneRange();
        if (!private_) {
            this.lastString = pattern;
            if (range == null) {
                this.cancel();
                this.found = false;
                return null;
            }
            this.found = true;
        }
        if (range && (!private_ || private_ < 0))
            this.selectedRange = range;
        return range;
    },

    get stale() {
        return this._stale || this.baseDocument.get() != this.content.document;
    },
    set stale(val) { this._stale = val; },

    addListeners: function addListeners() {
        for (let range of this.ranges)
            range.window.addEventListener("unload", this.bound.onUnload, true);
    },
    purgeListeners: function purgeListeners() {
        for (let range of this.ranges)
            try {
                range.window.removeEventListener("unload", this.bound.onUnload, true);
            }
            catch (e if e.result === Cr.NS_ERROR_FAILURE) {}
    },
    onUnload: function onUnload(event) {
        this.purgeListeners();
        if (this.highlighted)
            this.highlight(true);
        this.stale = true;
    }
}, {
    Range: Class("RangeFind.Range", {
        init: function init(range, index) {
            this.index = index;

            this.range = range;
            this.document = range.startContainer.ownerDocument;
            this.window = this.document.defaultView;

            if (this.selection == null)
                return false;

            this.save();
        },

        docShell: Class.Memoize(function () {
            return util.docShell(this.window);
        }),

        intersects: function (range) {
            return RangeFind.intersects(this.range, range);
        },

        save: function save() {
            this.scroll = Point(this.window.pageXOffset, this.window.pageYOffset);

            this.initialSelection = null;
            if (this.selection.rangeCount)
                this.initialSelection = this.selection.getRangeAt(0);
        },

        descroll: function descroll() {
            this.window.scrollTo(this.scroll.x, this.scroll.y);
        },

        deselect: function deselect() {
            if (this.selection) {
                this.selection.removeAllRanges();
                if (this.initialSelection)
                    this.selection.addRange(this.initialSelection);
            }
        },

        get selectionController() {
            return this.docShell
                       .QueryInterface(Ci.nsIInterfaceRequestor)
                       .getInterface(Ci.nsISelectionDisplay)
                       .QueryInterface(Ci.nsISelectionController);
        },
        get selection() {
            try {
                return this.selectionController.getSelection(Ci.nsISelectionController.SELECTION_NORMAL);
            }
            catch (e) {
                return null;
            }
        }
    }),
    contains: function contains(range, r, quiet) {
        try {
            return range.compareBoundaryPoints(range.START_TO_END, r) >= 0 &&
                   range.compareBoundaryPoints(range.END_TO_START, r) <= 0;
        }
        catch (e) {
            if (e.result != Cr.NS_ERROR_DOM_WRONG_DOCUMENT_ERR && !quiet)
                util.reportError(e, true);
            return false;
        }
    },
    containsNode: function containsNode(range, n, quiet) {
        return n.ownerDocument &&
               this.contains(range, RangeFind.nodeRange(n), quiet);
    },
    intersects: function intersects(range, r) {
        try {
            return r.compareBoundaryPoints(range.START_TO_END, range) >= 0 &&
                   r.compareBoundaryPoints(range.END_TO_START, range) <= 0;
        }
        catch (e) {
            util.reportError(e, true);
            return false;
        }
    },
    endpoint: function endpoint(range, before) {
        range = range.cloneRange();
        range.collapse(before);
        return range;
    },
    equal: function equal(r1, r2) {
        try {
            return !r1.compareBoundaryPoints(r1.START_TO_START, r2) && !r1.compareBoundaryPoints(r1.END_TO_END, r2);
        }
        catch (e) {
            return false;
        }
    },
    nodeContents: function nodeContents(node) {
        let range = node.ownerDocument.createRange();
        try {
            range.selectNodeContents(node);
        }
        catch (e) {}
        return range;
    },
    nodeRange: function nodeRange(node) {
        let range = node.ownerDocument.createRange();
        try {
            range.selectNode(node);
        }
        catch (e) {}
        return range;
    },
    sameDocument: function sameDocument(r1, r2) {
        if (!(r1 && r2 && r1.endContainer.ownerDocument == r2.endContainer.ownerDocument))
            return false;
        try {
            r1.compareBoundaryPoints(r1.START_TO_START, r2);
        }
        catch (e if e.result == 0x80530004 /* NS_ERROR_DOM_WRONG_DOCUMENT_ERR */) {
            return false;
        }
        return true;
    },
    selectNodePath: ["a", "xhtml:a", "*[@onclick]"].map(p => "ancestor-or-self::" + p).join(" | "),
    union: function union(a, b) {
        let start = a.compareBoundaryPoints(a.START_TO_START, b) < 0 ? a : b;
        let end   = a.compareBoundaryPoints(a.END_TO_END, b) > 0 ? a : b;
        let res   = start.cloneRange();
        res.setEnd(end.endContainer, end.endOffset);
        return res;
    }
});

// catch(e){ if (typeof e === "string") e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }

endModule();

// vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript:
