]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/finder.jsm
Import 1.0b7.1 supporting Firefox up to 8.*
[dactyl.git] / common / modules / finder.jsm
1 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
2 //
3 // This work is licensed for reuse under an MIT license. Details are
4 // given in the LICENSE.txt file included with this file.
5 "use strict";
6
7 Components.utils.import("resource://dactyl/bootstrap.jsm");
8 defineModule("finder", {
9     exports: ["RangeFind", "RangeFinder", "rangefinder"],
10     require: ["prefs"],
11     use: ["messages", "services", "util"]
12 }, this);
13
14 function equals(a, b) XPCNativeWrapper(a) == XPCNativeWrapper(b);
15
16 try {
17
18 /** @instance rangefinder */
19 var RangeFinder = Module("rangefinder", {
20     Local: function (dactyl, modules, window) ({
21         init: function () {
22             this.dactyl = dactyl;
23             this.modules = modules;
24             this.window = window;
25             this.lastFindPattern = "";
26         },
27
28         get rangeFind() {
29             let find = modules.buffer.localStore.rangeFind;
30             if (find && find.stale || !isinstance(find, RangeFind))
31                 return this.rangeFind = null;
32             return find;
33         },
34         set rangeFind(val) modules.buffer.localStore.rangeFind = val
35     }),
36
37     init: function init() {
38         prefs.safeSet("accessibility.typeaheadfind.autostart", false);
39         // The above should be sufficient, but: http://dactyl.sf.net/bmo/348187
40         prefs.safeSet("accessibility.typeaheadfind", false);
41     },
42
43     get commandline() this.modules.commandline,
44     get modes() this.modules.modes,
45     get options() this.modules.options,
46
47     openPrompt: function (mode) {
48         this.commandline;
49         this.CommandMode(mode).open();
50
51         if (this.rangeFind && equals(this.rangeFind.window.get(), this.window))
52             this.rangeFind.reset();
53         this.find("", mode == this.modes.FIND_BACKWARD);
54     },
55
56     bootstrap: function (str, backward) {
57         if (arguments.length < 2 && this.rangeFind)
58             backward = this.rangeFind.reverse;
59
60         let highlighted = this.rangeFind && this.rangeFind.highlighted;
61         let selections = this.rangeFind && this.rangeFind.selections;
62         let linksOnly = false;
63         let regexp = false;
64         let matchCase = this.options["findcase"] === "smart"  ? /[A-Z]/.test(str) :
65                         this.options["findcase"] === "ignore" ? false : true;
66
67         str = str.replace(/\\(.|$)/g, function (m, n1) {
68             if (n1 == "c")
69                 matchCase = false;
70             else if (n1 == "C")
71                 matchCase = true;
72             else if (n1 == "l")
73                 linksOnly = true;
74             else if (n1 == "L")
75                 linksOnly = false;
76             else if (n1 == "r")
77                 regexp = true;
78             else if (n1 == "R")
79                 regexp = false;
80             else
81                 return m;
82             return "";
83         });
84
85         // It's possible, with :tabdetach for instance, for the rangeFind to
86         // actually move from one window to another, which breaks things.
87         if (!this.rangeFind
88             || !equals(this.rangeFind.window.get(), this.window)
89             || linksOnly  != !!this.rangeFind.elementPath
90             || regexp     != this.rangeFind.regexp
91             || matchCase  != this.rangeFind.matchCase
92             || !!backward != this.rangeFind.reverse) {
93
94             if (this.rangeFind)
95                 this.rangeFind.cancel();
96             this.rangeFind = RangeFind(this.window, matchCase, backward,
97                                        linksOnly && this.options.get("hinttags").matcher,
98                                        regexp);
99             this.rangeFind.highlighted = highlighted;
100             this.rangeFind.selections = selections;
101         }
102         return this.lastFindPattern = str;
103     },
104
105     find: function (pattern, backwards) {
106         let str = this.bootstrap(pattern, backwards);
107         if (!this.rangeFind.find(str))
108             this.dactyl.echoerr(_("finder.notFound", pattern),
109                                 this.commandline.FORCE_SINGLELINE);
110
111         return this.rangeFind.found;
112     },
113
114     findAgain: function (reverse) {
115         if (!this.rangeFind)
116             this.find(this.lastFindPattern);
117         else if (!this.rangeFind.find(null, reverse))
118             this.dactyl.echoerr(_("finder.notFound", this.lastFindPattern),
119                                 this.commandline.FORCE_SINGLELINE);
120         else if (this.rangeFind.wrapped) {
121             let msg = this.rangeFind.backward ? _("finder.atTop")
122                                               : _("finder.atBottom");
123             this.commandline.echo(msg, "WarningMsg", this.commandline.APPEND_TO_MESSAGES
124                                                    | this.commandline.FORCE_SINGLELINE);
125         }
126         else
127             this.commandline.echo((this.rangeFind.backward ? "?" : "/") + this.lastFindPattern,
128                                   "Normal", this.commandline.FORCE_SINGLELINE);
129
130         if (this.options["hlfind"])
131             this.highlight();
132         this.rangeFind.focus();
133     },
134
135     onCancel: function () {
136         if (this.rangeFind)
137             this.rangeFind.cancel();
138     },
139
140     onChange: function (command) {
141         if (this.options["incfind"]) {
142             command = this.bootstrap(command);
143             this.rangeFind.find(command);
144         }
145     },
146
147     onHistory: function () {
148         this.rangeFind.found = false;
149     },
150
151     onSubmit: function (command) {
152         if (!this.options["incfind"] || !this.rangeFind || !this.rangeFind.found) {
153             this.clear();
154             this.find(command || this.lastFindPattern, this.modes.extended & this.modes.FIND_BACKWARD);
155         }
156
157         if (this.options["hlfind"])
158             this.highlight();
159         this.rangeFind.focus();
160     },
161
162     /**
163      * Highlights all occurrences of the last sought for string in the
164      * current buffer.
165      */
166     highlight: function () {
167         if (this.rangeFind)
168             this.rangeFind.highlight();
169     },
170
171     /**
172      * Clears all find highlighting.
173      */
174     clear: function () {
175         if (this.rangeFind)
176             this.rangeFind.highlight(true);
177     }
178 }, {
179 }, {
180     modes: function initModes(dactyl, modules, window) {
181         initModes.require("commandline");
182
183         const { modes } = modules;
184
185         modes.addMode("FIND", {
186             description: "Find mode, active when typing search input",
187             bases: [modes.COMMAND_LINE]
188         });
189         modes.addMode("FIND_FORWARD", {
190             description: "Forward Find mode, active when typing search input",
191             bases: [modes.FIND]
192         });
193         modes.addMode("FIND_BACKWARD", {
194             description: "Backward Find mode, active when typing search input",
195             bases: [modes.FIND]
196         });
197     },
198     commands: function initCommands(dactyl, modules, window) {
199         const { commands, rangefinder } = modules;
200         commands.add(["noh[lfind]"],
201             "Remove the find highlighting",
202             function () { rangefinder.clear(); },
203             { argCount: "0" });
204     },
205     commandline: function initCommandline(dactyl, modules, window) {
206         const { rangefinder } = modules;
207         rangefinder.CommandMode = Class("CommandFindMode", modules.CommandMode, {
208             init: function init(mode) {
209                 this.mode = mode;
210                 init.supercall(this);
211             },
212
213             historyKey: "find",
214
215             get prompt() this.mode === modules.modes.FIND_BACKWARD ? "?" : "/",
216
217             get onCancel() modules.rangefinder.closure.onCancel,
218             get onChange() modules.rangefinder.closure.onChange,
219             get onHistory() modules.rangefinder.closure.onHistory,
220             get onSubmit() modules.rangefinder.closure.onSubmit
221         });
222     },
223     mappings: function (dactyl, modules, window) {
224         const { Buffer, buffer, config, mappings, modes, rangefinder } = modules;
225         var myModes = config.browserModes.concat([modes.CARET]);
226
227         mappings.add(myModes,
228             ["/", "<find-forward>"], "Find a pattern starting at the current caret position",
229             function () { rangefinder.openPrompt(modes.FIND_FORWARD); });
230
231         mappings.add(myModes,
232             ["?", "<find-backward>"], "Find a pattern backward of the current caret position",
233             function () { rangefinder.openPrompt(modes.FIND_BACKWARD); });
234
235         mappings.add(myModes,
236             ["n", "<find-next>"], "Find next",
237             function () { rangefinder.findAgain(false); });
238
239         mappings.add(myModes,
240             ["N", "<find-previous>"], "Find previous",
241             function () { rangefinder.findAgain(true); });
242
243         mappings.add(myModes.concat([modes.CARET, modes.TEXT_EDIT]), ["*", "<find-word-forward>"],
244             "Find word under cursor",
245             function () {
246                 rangefinder.find(Buffer.currentWord(buffer.focusedFrame, true), false);
247                 rangefinder.findAgain();
248             });
249
250         mappings.add(myModes.concat([modes.CARET, modes.TEXT_EDIT]), ["#", "<find-word-backward>"],
251             "Find word under cursor backwards",
252             function () {
253                 rangefinder.find(Buffer.currentWord(buffer.focusedFrame, true), true);
254                 rangefinder.findAgain();
255             });
256
257     },
258     options: function (dactyl, modules, window) {
259         const { options, rangefinder } = modules;
260         const { prefs } = require("prefs");
261
262         options.add(["hlfind", "hlf"],
263             "Highlight all /find pattern matches on the current page after submission",
264             "boolean", false, {
265                 setter: function (value) {
266                     rangefinder[value ? "highlight" : "clear"]();
267                     return value;
268                 }
269             });
270
271         options.add(["findcase", "fc"],
272             "Find case matching mode",
273             "string", "smart",
274             {
275                 values: {
276                     "smart": "Case is significant when capital letters are typed",
277                     "match": "Case is always significant",
278                     "ignore": "Case is never significant"
279                 }
280             });
281
282         options.add(["incfind", "if"],
283             "Find a pattern incrementally as it is typed rather than awaiting c_<Return>",
284             "boolean", true);
285     }
286 });
287
288 /**
289  * @class RangeFind
290  *
291  * A fairly sophisticated typeahead-find replacement. It supports
292  * incremental find very much as the builtin component.
293  * Additionally, it supports several features impossible to
294  * implement using the standard component. Incremental finding
295  * works both forwards and backwards. Erasing characters during an
296  * incremental find moves the selection back to the first
297  * available match for the shorter term. The selection and viewport
298  * are restored when the find is canceled.
299  *
300  * Also, in addition to full support for frames and iframes, this
301  * implementation will begin finding from the position of the
302  * caret in the last active frame. This is contrary to the behavior
303  * of the builtin component, which always starts a find from the
304  * beginning of the first frame in the case of frameset documents,
305  * and cycles through all frames from beginning to end. This makes it
306  * impossible to choose the starting point of a find for such
307  * documents, and represents a major detriment to productivity where
308  * large amounts of data are concerned (e.g., for API documents).
309  */
310 var RangeFind = Class("RangeFind", {
311     init: function init(window, matchCase, backward, elementPath, regexp) {
312         this.window = Cu.getWeakReference(window);
313         this.content = window.content;
314
315         this.baseDocument = Cu.getWeakReference(this.content.document);
316         this.elementPath = elementPath || null;
317         this.reverse = Boolean(backward);
318
319         this.finder = services.Find();
320         this.matchCase = Boolean(matchCase);
321         this.regexp = Boolean(regexp);
322
323         this.reset();
324
325         this.highlighted = null;
326         this.selections = [];
327         this.lastString = "";
328     },
329
330     get store() this.content.document.dactylStore = this.content.document.dactylStore || {},
331
332     get backward() this.finder.findBackwards,
333
334     get matchCase() this.finder.caseSensitive,
335     set matchCase(val) this.finder.caseSensitive = Boolean(val),
336
337     get regexp() this.finder.regularExpression || false,
338     set regexp(val) {
339         try {
340             return this.finder.regularExpression = Boolean(val);
341         }
342         catch (e) {
343             return false;
344         }
345     },
346
347     get findString() this.lastString,
348
349     get selectedRange() {
350         let win = this.store.focusedFrame && this.store.focusedFrame.get() || this.content;
351
352         let selection = win.getSelection();
353         return (selection.rangeCount ? selection.getRangeAt(0) : this.ranges[0].range).cloneRange();
354     },
355     set selectedRange(range) {
356         this.range.selection.removeAllRanges();
357         this.range.selection.addRange(range);
358         this.range.selectionController.scrollSelectionIntoView(
359             this.range.selectionController.SELECTION_NORMAL, 0, false);
360
361         this.store.focusedFrame = Cu.getWeakReference(range.startContainer.ownerDocument.defaultView);
362     },
363
364     cancel: function cancel() {
365         this.purgeListeners();
366         this.range.deselect();
367         this.range.descroll();
368     },
369
370     compareRanges: function compareRanges(r1, r2) {
371         try {
372             return this.backward ?  r1.compareBoundaryPoints(r1.END_TO_START, r2)
373                                  : -r1.compareBoundaryPoints(r1.START_TO_END, r2);
374         }
375         catch (e) {
376             util.reportError(e);
377             return 0;
378         }
379     },
380
381     findRange: function findRange(range) {
382         let doc = range.startContainer.ownerDocument;
383         let win = doc.defaultView;
384         let ranges = this.ranges.filter(function (r)
385             r.window === win && RangeFind.sameDocument(r.range, range) && RangeFind.contains(r.range, range));
386
387         if (this.backward)
388             return ranges[ranges.length - 1];
389         return ranges[0];
390     },
391
392     findSubRanges: function findSubRanges(range) {
393         let doc = range.startContainer.ownerDocument;
394         for (let elem in this.elementPath(doc)) {
395             let r = RangeFind.nodeRange(elem);
396             if (RangeFind.contains(range, r))
397                 yield r;
398         }
399     },
400
401     focus: function focus() {
402         if (this.lastRange)
403             var node = util.evaluateXPath(RangeFind.selectNodePath,
404                                           this.lastRange.commonAncestorContainer).snapshotItem(0);
405         if (node) {
406             node.focus();
407             // Re-highlight collapsed selection
408             this.selectedRange = this.lastRange;
409         }
410     },
411
412     highlight: function highlight(clear) {
413         if (!clear && (!this.lastString || this.lastString == this.highlighted))
414             return;
415         if (clear && !this.highlighted)
416             return;
417
418         if (!clear && this.highlighted)
419             this.highlight(true);
420
421         if (clear) {
422             this.selections.forEach(function (selection) {
423                 selection.removeAllRanges();
424             });
425             this.selections = [];
426             this.highlighted = null;
427         }
428         else {
429             this.selections = [];
430             let string = this.lastString;
431             for (let r in this.iter(string)) {
432                 let controller = this.range.selectionController;
433                 for (let node = r.startContainer; node; node = node.parentNode)
434                     if (node instanceof Ci.nsIDOMNSEditableElement) {
435                         controller = node.editor.selectionController;
436                         break;
437                     }
438
439                 let sel = controller.getSelection(Ci.nsISelectionController.SELECTION_FIND);
440                 sel.addRange(r);
441                 if (this.selections.indexOf(sel) < 0)
442                     this.selections.push(sel);
443             }
444             this.highlighted = this.lastString;
445             if (this.lastRange)
446                 this.selectedRange = this.lastRange;
447             this.addListeners();
448         }
449     },
450
451     indexIter: function (private_) {
452         let idx = this.range.index;
453         if (this.backward)
454             var groups = [util.range(idx + 1, 0, -1), util.range(this.ranges.length, idx, -1)];
455         else
456             var groups = [util.range(idx, this.ranges.length), util.range(0, idx + 1)];
457
458         for (let i in groups[0])
459             yield i;
460
461         if (!private_) {
462             this.wrapped = true;
463             this.lastRange = null;
464             for (let i in groups[1])
465                 yield i;
466         }
467     },
468
469     iter: function (word) {
470         let saved = ["lastRange", "lastString", "range"].map(function (s) [s, this[s]], this);
471         try {
472             this.range = this.ranges[0];
473             this.lastRange = null;
474             this.lastString = word;
475             var res;
476             while (res = this.find(null, this.reverse, true))
477                 yield res;
478         }
479         finally {
480             saved.forEach(function ([k, v]) this[k] = v, this);
481         }
482     },
483
484     makeFrameList: function (win) {
485         const self = this;
486         win = win.top;
487         let frames = [];
488         let backup = null;
489
490         function pushRange(start, end) {
491             function push(r) {
492                 if (r = RangeFind.Range(r, frames.length))
493                     frames.push(r);
494             }
495
496             let doc = start.startContainer.ownerDocument;
497
498             let range = doc.createRange();
499             range.setStart(start.startContainer, start.startOffset);
500             range.setEnd(end.startContainer, end.startOffset);
501
502             if (!self.elementPath)
503                 push(range);
504             else
505                 for (let r in self.findSubRanges(range))
506                     push(r);
507         }
508         function rec(win) {
509             let doc = win.document;
510             let pageRange = RangeFind[doc.body ? "nodeRange" : "nodeContents"](doc.body || doc.documentElement);
511             backup = backup || pageRange;
512             let pageStart = RangeFind.endpoint(pageRange, true);
513             let pageEnd = RangeFind.endpoint(pageRange, false);
514
515             for (let frame in array.iterValues(win.frames)) {
516                 let range = doc.createRange();
517                 if (util.computedStyle(frame.frameElement).visibility == "visible") {
518                     range.selectNode(frame.frameElement);
519                     pushRange(pageStart, RangeFind.endpoint(range, true));
520                     pageStart = RangeFind.endpoint(range, false);
521                     rec(frame);
522                 }
523             }
524             pushRange(pageStart, pageEnd);
525
526             let anonNodes = doc.getAnonymousNodes(doc.documentElement);
527             if (anonNodes) {
528                 for (let [, elem] in iter(anonNodes)) {
529                     let range = RangeFind.nodeContents(elem);
530                     pushRange(RangeFind.endpoint(range, true), RangeFind.endpoint(range, false));
531                 }
532             }
533         }
534         rec(win);
535         if (frames.length == 0)
536             frames[0] = RangeFind.Range(RangeFind.endpoint(backup, true), 0);
537         return frames;
538     },
539
540     reset: function () {
541         this.ranges = this.makeFrameList(this.content);
542
543         this.startRange = this.selectedRange;
544         this.startRange.collapse(!this.reverse);
545         this.lastRange = this.selectedRange;
546         this.range = this.findRange(this.startRange);
547         this.ranges.first = this.range;
548         this.ranges.forEach(function (range) range.save());
549         this.forward = null;
550         this.found = false;
551     },
552
553     // This doesn't work yet.
554     resetCaret: function () {
555         let equal = RangeFind.equal;
556         let selection = this.win.getSelection();
557         if (selection.rangeCount == 0)
558             selection.addRange(this.pageStart);
559         function getLines() {
560             let orig = selection.getRangeAt(0);
561             function getRanges(forward) {
562                 selection.removeAllRanges();
563                 selection.addRange(orig);
564                 let cur = orig;
565                 while (true) {
566                     var last = cur;
567                     this.sel.lineMove(forward, false);
568                     cur = selection.getRangeAt(0);
569                     if (equal(cur, last))
570                         break;
571                     yield cur;
572                 }
573             }
574             yield orig;
575             for (let range in getRanges(true))
576                 yield range;
577             for (let range in getRanges(false))
578                 yield range;
579         }
580         for (let range in getLines()) {
581             if (this.sel.checkVisibility(range.startContainer, range.startOffset, range.startOffset))
582                 return range;
583         }
584         return null;
585     },
586
587     find: function (word, reverse, private_) {
588         if (!private_ && this.lastRange && !RangeFind.equal(this.selectedRange, this.lastRange))
589             this.reset();
590
591         this.wrapped = false;
592         this.finder.findBackwards = reverse ? !this.reverse : this.reverse;
593         let again = word == null;
594         if (again)
595             word = this.lastString;
596         if (!this.matchCase)
597             word = word.toLowerCase();
598
599         if (!again && (word === "" || word.indexOf(this.lastString) !== 0 || this.backward)) {
600             if (!private_)
601                 this.range.deselect();
602             if (word === "")
603                 this.range.descroll();
604             this.lastRange = this.startRange;
605             this.range = this.ranges.first;
606         }
607
608         if (word == "")
609             var range = this.startRange;
610         else
611             for (let i in this.indexIter(private_)) {
612                 if (!private_ && this.range.window != this.ranges[i].window && this.range.window != this.ranges[i].window.parent) {
613                     this.range.descroll();
614                     this.range.deselect();
615                 }
616                 this.range = this.ranges[i];
617
618                 let start = RangeFind.sameDocument(this.lastRange, this.range.range) && this.range.intersects(this.lastRange) ?
619                                 RangeFind.endpoint(this.lastRange, !(again ^ this.backward)) :
620                                 RangeFind.endpoint(this.range.range, !this.backward);
621
622                 if (this.backward && !again)
623                     start = RangeFind.endpoint(this.startRange, false);
624
625                 var range = this.finder.Find(word, this.range.range, start, this.range.range);
626                 if (range)
627                     break;
628             }
629
630         if (range)
631             this.lastRange = range.cloneRange();
632         if (!private_) {
633             this.lastString = word;
634             if (range == null) {
635                 this.cancel();
636                 this.found = false;
637                 return null;
638             }
639             this.found = true;
640         }
641         if (range && (!private_ || private_ < 0))
642             this.selectedRange = range;
643         return range;
644     },
645
646     get stale() this._stale || this.baseDocument.get() != this.content.document,
647     set stale(val) this._stale = val,
648
649     addListeners: function () {
650         for (let range in array.iterValues(this.ranges))
651             range.window.addEventListener("unload", this.closure.onUnload, true);
652     },
653     purgeListeners: function () {
654         for (let range in array.iterValues(this.ranges))
655             try {
656                 range.window.removeEventListener("unload", this.closure.onUnload, true);
657             }
658             catch (e if e.result === Cr.NS_ERROR_FAILURE) {}
659     },
660     onUnload: function (event) {
661         this.purgeListeners();
662         if (this.highlighted)
663             this.highlight(true);
664         this.stale = true;
665     }
666 }, {
667     Range: Class("RangeFind.Range", {
668         init: function (range, index) {
669             this.index = index;
670
671             this.range = range;
672             this.document = range.startContainer.ownerDocument;
673             this.window = this.document.defaultView;
674             this.docShell = this.window.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebNavigation)
675                                        .QueryInterface(Ci.nsIDocShell);
676
677             if (this.selection == null)
678                 return false;
679
680             this.save();
681         },
682
683         intersects: function (range) RangeFind.intersects(this.range, range),
684
685         save: function () {
686             this.scroll = Point(this.window.pageXOffset, this.window.pageYOffset);
687
688             this.initialSelection = null;
689             if (this.selection.rangeCount)
690                 this.initialSelection = this.selection.getRangeAt(0);
691         },
692
693         descroll: function () {
694             this.window.scrollTo(this.scroll.x, this.scroll.y);
695         },
696
697         deselect: function () {
698             if (this.selection) {
699                 this.selection.removeAllRanges();
700                 if (this.initialSelection)
701                     this.selection.addRange(this.initialSelection);
702             }
703         },
704
705         get selectionController() this.docShell
706                     .QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsISelectionDisplay)
707                     .QueryInterface(Ci.nsISelectionController),
708         get selection() {
709             try {
710                 return this.selectionController.getSelection(Ci.nsISelectionController.SELECTION_NORMAL);
711             }
712             catch (e) {
713                 return null;
714             }
715         }
716     }),
717     contains: function (range, r) {
718         try {
719             return range.compareBoundaryPoints(range.START_TO_END, r) >= 0 &&
720                    range.compareBoundaryPoints(range.END_TO_START, r) <= 0;
721         }
722         catch (e) {
723             util.reportError(e, true);
724             return false;
725         }
726     },
727     intersects: function (range, r) {
728         try {
729             return r.compareBoundaryPoints(range.START_TO_END, range) >= 0 &&
730                    r.compareBoundaryPoints(range.END_TO_START, range) <= 0;
731         }
732         catch (e) {
733             util.reportError(e, true);
734             return false;
735         }
736     },
737     endpoint: function (range, before) {
738         range = range.cloneRange();
739         range.collapse(before);
740         return range;
741     },
742     equal: function (r1, r2) {
743         try {
744             return !r1.compareBoundaryPoints(r1.START_TO_START, r2) && !r1.compareBoundaryPoints(r1.END_TO_END, r2);
745         }
746         catch (e) {
747             return false;
748         }
749     },
750     nodeContents: function (node) {
751         let range = node.ownerDocument.createRange();
752         try {
753             range.selectNodeContents(node);
754         }
755         catch (e) {}
756         return range;
757     },
758     nodeRange: function (node) {
759         let range = node.ownerDocument.createRange();
760         try {
761             range.selectNode(node);
762         }
763         catch (e) {}
764         return range;
765     },
766     sameDocument: function (r1, r2) {
767         if (!(r1 && r2 && r1.endContainer.ownerDocument == r2.endContainer.ownerDocument))
768             return false;
769         try {
770             r1.compareBoundaryPoints(r1.START_TO_START, r2);
771         }
772         catch (e if e.result == 0x80530004 /* NS_ERROR_DOM_WRONG_DOCUMENT_ERR */) {
773             return false;
774         }
775         return true;
776     },
777     selectNodePath: ["a", "xhtml:a", "*[@onclick]"].map(function (p) "ancestor-or-self::" + p).join(" | ")
778 });
779
780 } catch(e){ if (typeof e === "string") e = Error(e); dump(e.fileName+":"+e.lineNumber+": "+e+"\n" + e.stack); }
781
782 endModule();
783
784
785 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: