]> git.donarmstrong.com Git - dactyl.git/blob - common/content/hints.js
Import 1.0b7.1 supporting Firefox up to 8.*
[dactyl.git] / common / content / hints.js
1 // Copyright (c) 2006-2008 by Martin Stubenschrott <stubenschrott@vimperator.org>
2 // Copyright (c) 2007-2011 by Doug Kearns <dougkearns@gmail.com>
3 // Copyright (c) 2008-2011 by Kris Maglione <maglione.k@gmail.com>
4 //
5 // This work is licensed for reuse under an MIT license. Details are
6 // given in the LICENSE.txt file included with this file.
7 "use strict";
8
9 /** @scope modules */
10 /** @instance hints */
11
12 var HintSession = Class("HintSession", CommandMode, {
13     get extendedMode() modes.HINTS,
14
15     init: function init(mode, opts) {
16         init.supercall(this);
17
18         opts = opts || {};
19
20         // Hack.
21         if (!opts.window && modes.main == modes.OUTPUT_MULTILINE)
22             opts.window = commandline.widgets.multilineOutput.contentWindow;
23
24         this.hintMode = hints.modes[mode];
25         dactyl.assert(this.hintMode);
26
27         this.activeTimeout = null; // needed for hinttimeout > 0
28         this.continue = Boolean(opts.continue);
29         this.docs = [];
30         this.hintKeys = events.fromString(options["hintkeys"]).map(events.closure.toString);
31         this.hintNumber = 0;
32         this.hintString = opts.filter || "";
33         this.pageHints = [];
34         this.prevInput = "";
35         this.usedTabKey = false;
36         this.validHints = []; // store the indices of the "hints" array with valid elements
37
38         this.open();
39
40         this.top = opts.window || content;
41         this.top.addEventListener("resize", this.closure._onResize, true);
42         this.top.addEventListener("dactyl-commandupdate", this.closure._onResize, false, true);
43
44         this.generate();
45
46         this.show();
47         this.magic = true;
48
49         if (this.validHints.length == 0) {
50             dactyl.beep();
51             modes.pop();
52         }
53         else if (this.validHints.length == 1 && !this.continue)
54             this.process(false);
55         else
56             this.checkUnique();
57     },
58
59     Hint: {
60         get active() this._active,
61         set active(val) {
62             this._active = val;
63             if (val)
64                 this.span.setAttribute("active", true);
65             else
66                 this.span.removeAttribute("active");
67
68             hints.setClass(this.elem, this.valid ? val : null);
69             if (this.imgSpan)
70                 hints.setClass(this.imgSpan, this.valid ? val : null);
71         },
72
73         get ambiguous() this.span.hasAttribute("ambiguous"),
74         set ambiguous(val) {
75             let meth = val ? "setAttribute" : "removeAttribute";
76             this.elem[meth]("ambiguous", "true");
77             this.span[meth]("ambiguous", "true");
78             if (this.imgSpan)
79                 this.imgSpan[meth]("ambiguous", "true");
80         },
81
82         get valid() this._valid,
83         set valid(val) {
84             this._valid = val,
85
86             this.span.style.display = (val ? "" : "none");
87             if (this.imgSpan)
88                 this.imgSpan.style.display = (val ? "" : "none");
89             this.active = this.active;
90         }
91     },
92
93     get mode() modes.HINTS,
94
95     get prompt() ["Question", UTF8(this.hintMode.prompt) + ": "],
96
97     leave: function leave(stack) {
98         leave.superapply(this, arguments);
99
100         if (!stack.push) {
101             if (hints.hintSession == this)
102                 hints.hintSession = null;
103             if (this.top) {
104                 this.top.removeEventListener("resize", this.closure._onResize, true);
105                 this.top.removeEventListener("dactyl-commandupdate", this.closure._onResize, true);
106             }
107
108             this.removeHints(0);
109         }
110     },
111
112     checkUnique: function _checkUnique() {
113         if (this.hintNumber == 0)
114             return;
115         dactyl.assert(this.hintNumber <= this.validHints.length);
116
117         // if we write a numeric part like 3, but we have 45 hints, only follow
118         // the hint after a timeout, as the user might have wanted to follow link 34
119         if (this.hintNumber > 0 && this.hintNumber * this.hintKeys.length <= this.validHints.length) {
120             let timeout = options["hinttimeout"];
121             if (timeout > 0)
122                 this.activeTimeout = this.timeout(function () {
123                     this.process(true);
124                 }, timeout);
125         }
126         else // we have a unique hint
127             this.process(true);
128     },
129
130     /**
131      * Clear any timeout which might be active after pressing a number
132      */
133     clearTimeout: function () {
134         if (this.activeTimeout)
135             this.activeTimeout.cancel();
136         this.activeTimeout = null;
137     },
138
139     _escapeNumbers: false,
140     get escapeNumbers() this._escapeNumbers,
141     set escapeNumbers(val) {
142         this.clearTimeout();
143         this._escapeNumbers = !!val;
144         if (val && this.usedTabKey)
145             this.hintNumber = 0;
146
147         this.updateStatusline();
148     },
149
150     /**
151      * Returns the hint string for a given number based on the values of
152      * the 'hintkeys' option.
153      *
154      * @param {number} n The number to transform.
155      * @returns {string}
156      */
157     getHintString: function getHintString(n) {
158         let res = [], len = this.hintKeys.length;
159         do {
160             res.push(this.hintKeys[n % len]);
161             n = Math.floor(n / len);
162         }
163         while (n > 0);
164         return res.reverse().join("");
165     },
166
167     /**
168      * The reverse of {@link #getHintString}. Given a hint string,
169      * returns its index.
170      *
171      * @param {string} str The hint's string.
172      * @returns {number} The hint's index.
173      */
174     getHintNumber: function getHintNumber(str) {
175         let base = this.hintKeys.length;
176         let res = 0;
177         for (let char in values(str))
178             res = res * base + this.hintKeys.indexOf(char);
179         return res;
180     },
181
182     /**
183      * Returns true if the given key string represents a
184      * pseudo-hint-number.
185      *
186      * @param {string} key The key to test.
187      * @returns {boolean} Whether the key represents a hint number.
188      */
189     isHintKey: function isHintKey(key) this.hintKeys.indexOf(key) >= 0,
190
191     /**
192      * Gets the actual offset of an imagemap area.
193      *
194      * Only called by {@link #_generate}.
195      *
196      * @param {Object} elem The <area> element.
197      * @param {number} leftPos The left offset of the image.
198      * @param {number} topPos The top offset of the image.
199      * @returns [leftPos, topPos] The updated offsets.
200      */
201     getAreaOffset: function _getAreaOffset(elem, leftPos, topPos) {
202         try {
203             // Need to add the offset to the area element.
204             // Always try to find the top-left point, as per dactyl default.
205             let shape = elem.getAttribute("shape").toLowerCase();
206             let coordStr = elem.getAttribute("coords");
207             // Technically it should be only commas, but hey
208             coordStr = coordStr.replace(/\s+[;,]\s+/g, ",").replace(/\s+/g, ",");
209             let coords = coordStr.split(",").map(Number);
210
211             if ((shape == "rect" || shape == "rectangle") && coords.length == 4) {
212                 leftPos += coords[0];
213                 topPos += coords[1];
214             }
215             else if (shape == "circle" && coords.length == 3) {
216                 leftPos += coords[0] - coords[2] / Math.sqrt(2);
217                 topPos += coords[1] - coords[2] / Math.sqrt(2);
218             }
219             else if ((shape == "poly" || shape == "polygon") && coords.length % 2 == 0) {
220                 let leftBound = Infinity;
221                 let topBound = Infinity;
222
223                 // First find the top-left corner of the bounding rectangle (offset from image topleft can be noticeably suboptimal)
224                 for (let i = 0; i < coords.length; i += 2) {
225                     leftBound = Math.min(coords[i], leftBound);
226                     topBound = Math.min(coords[i + 1], topBound);
227                 }
228
229                 let curTop = null;
230                 let curLeft = null;
231                 let curDist = Infinity;
232
233                 // Then find the closest vertex. (we could generalize to nearest point on an edge, but I doubt there is a need)
234                 for (let i = 0; i < coords.length; i += 2) {
235                     let leftOffset = coords[i] - leftBound;
236                     let topOffset = coords[i + 1] - topBound;
237                     let dist = Math.sqrt(leftOffset * leftOffset + topOffset * topOffset);
238                     if (dist < curDist) {
239                         curDist = dist;
240                         curLeft = coords[i];
241                         curTop = coords[i + 1];
242                     }
243                 }
244
245                 // If we found a satisfactory offset, let's use it.
246                 if (curDist < Infinity)
247                     return [leftPos + curLeft, topPos + curTop];
248             }
249         }
250         catch (e) {} // badly formed document, or shape == "default" in which case we don't move the hint
251         return [leftPos, topPos];
252     },
253
254     // the containing block offsets with respect to the viewport
255     getContainerOffsets: function _getContainerOffsets(doc) {
256         let body = doc.body || doc.documentElement;
257         // TODO: getComputedStyle returns null for Facebook channel_iframe doc - probable Gecko bug.
258         let style = util.computedStyle(body);
259
260         if (style && /^(absolute|fixed|relative)$/.test(style.position)) {
261             let rect = body.getClientRects()[0];
262             return [-rect.left, -rect.top];
263         }
264         else
265             return [doc.defaultView.scrollX, doc.defaultView.scrollY];
266     },
267
268     /**
269      * Generate the hints in a window.
270      *
271      * Pushes the hints into the pageHints object, but does not display them.
272      *
273      * @param {Window} win The window for which to generate hints.
274      * @default content
275      */
276     generate: function _generate(win, offsets) {
277         if (!win)
278             win = this.top;
279
280         let doc = win.document;
281
282         memoize(doc, "dactylLabels", function ()
283             iter([l.getAttribute("for"), l]
284                  for (l in array.iterValues(doc.querySelectorAll("label[for]"))))
285              .toObject());
286
287         let [offsetX, offsetY] = this.getContainerOffsets(doc);
288
289         offsets = offsets || { left: 0, right: 0, top: 0, bottom: 0 };
290         offsets.right  = win.innerWidth  - offsets.right;
291         offsets.bottom = win.innerHeight - offsets.bottom;
292
293         function isVisible(elem) {
294             let rect = elem.getBoundingClientRect();
295             if (!rect ||
296                 rect.top > offsets.bottom || rect.bottom < offsets.top ||
297                 rect.left > offsets.right || rect.right < offsets.left)
298                 return false;
299
300             if (!rect.width || !rect.height)
301                 if (!Array.some(elem.childNodes, function (elem) elem instanceof Element && util.computedStyle(elem).float != "none" && isVisible(elem)))
302                     return false;
303
304             let computedStyle = doc.defaultView.getComputedStyle(elem, null);
305             if (computedStyle.visibility != "visible" || computedStyle.display == "none")
306                 return false;
307             return true;
308         }
309
310         let body = doc.body || doc.querySelector("body");
311         if (body) {
312             let fragment = util.xmlToDom(<div highlight="hints"/>, doc);
313             body.appendChild(fragment);
314             util.computedStyle(fragment).height; // Force application of binding.
315             let container = doc.getAnonymousElementByAttribute(fragment, "anonid", "hints") || fragment;
316
317             let baseNodeAbsolute = util.xmlToDom(<span highlight="Hint" style="display: none;"/>, doc);
318
319             let mode = this.hintMode;
320             let res = mode.matcher(doc);
321
322             let start = this.pageHints.length;
323             let _hints = [];
324             for (let elem in res)
325                 if (isVisible(elem) && (!mode.filter || mode.filter(elem)))
326                     _hints.push({
327                         elem: elem,
328                         rect: elem.getClientRects()[0] || elem.getBoundingClientRect(),
329                         showText: false,
330                         __proto__: this.Hint
331                     });
332
333             for (let hint in values(_hints)) {
334                 let { elem, rect } = hint;
335
336                 if (elem.hasAttributeNS(NS, "hint"))
337                     [hint.text, hint.showText] = [elem.getAttributeNS(NS, "hint"), true];
338                 else if (isinstance(elem, [HTMLInputElement, HTMLSelectElement, HTMLTextAreaElement]))
339                     [hint.text, hint.showText] = hints.getInputHint(elem, doc);
340                 else if (elem.firstElementChild instanceof HTMLImageElement && /^\s*$/.test(elem.textContent))
341                     [hint.text, hint.showText] = [elem.firstElementChild.alt || elem.firstElementChild.title, true];
342                 else
343                     hint.text = elem.textContent.toLowerCase();
344
345                 hint.span = baseNodeAbsolute.cloneNode(false);
346
347                 let leftPos = Math.max((rect.left + offsetX), offsetX);
348                 let topPos  = Math.max((rect.top + offsetY), offsetY);
349
350                 if (elem instanceof HTMLAreaElement)
351                     [leftPos, topPos] = this.getAreaOffset(elem, leftPos, topPos);
352
353                 hint.span.setAttribute("style", ["display: none; left:", leftPos, "px; top:", topPos, "px"].join(""));
354                 container.appendChild(hint.span);
355
356                 this.pageHints.push(hint);
357             }
358
359             this.docs.push({ doc: doc, start: start, end: this.pageHints.length - 1 });
360         }
361
362         Array.forEach(win.frames, function (f) {
363             if (isVisible(f.frameElement)) {
364                 let rect = f.frameElement.getBoundingClientRect();
365                 this.generate(f, {
366                     left: Math.max(offsets.left - rect.left, 0),
367                     right: Math.max(rect.right - offsets.right, 0),
368                     top: Math.max(offsets.top - rect.top, 0),
369                     bottom: Math.max(rect.bottom - offsets.bottom, 0)
370                 });
371             }
372         }, this);
373
374         return true;
375     },
376
377     /**
378      * Handle user input.
379      *
380      * Will update the filter on displayed hints and follow the final hint if
381      * necessary.
382      *
383      * @param {Event} event The keypress event.
384      */
385     onChange: function onChange(event) {
386         this.prevInput = "text";
387
388         this.clearTimeout();
389
390         this.hintNumber = 0;
391         this.hintString = commandline.command;
392         this.updateStatusline();
393         this.show();
394         if (this.validHints.length == 1)
395             this.process(false);
396     },
397
398     /**
399      * Handle a hints mode event.
400      *
401      * @param {Event} event The event to handle.
402      */
403     onKeyPress: function onKeyPress(eventList) {
404         const KILL = false, PASS = true;
405         let key = events.toString(eventList[0]);
406
407         this.clearTimeout();
408
409         if (!this.escapeNumbers && this.isHintKey(key)) {
410             this.prevInput = "number";
411
412             let oldHintNumber = this.hintNumber;
413             if (this.usedTabKey) {
414                 this.hintNumber = 0;
415                 this.usedTabKey = false;
416             }
417             this.hintNumber = this.hintNumber * this.hintKeys.length +
418                 this.hintKeys.indexOf(key);
419
420             this.updateStatusline();
421
422             if (this.docs.length)
423                 this.updateValidNumbers();
424             else {
425                 this.generate();
426                 this.show();
427             }
428
429             this.showActiveHint(this.hintNumber, oldHintNumber || 1);
430
431             dactyl.assert(this.hintNumber != 0);
432
433             this.checkUnique();
434             return KILL;
435         }
436
437         return PASS;
438     },
439
440     onResize: function onResize() {
441         this.removeHints(0);
442         this.generate(this.top);
443         this.show();
444     },
445
446     _onResize: function _onResize() {
447         if (this.magic)
448             hints.resizeTimer.tell();
449     },
450
451     /**
452      * Finish hinting.
453      *
454      * Called when there are one or zero hints in order to possibly activate it
455      * and, if activated, to clean up the rest of the hinting system.
456      *
457      * @param {boolean} followFirst Whether to force the following of the first
458      *     link (when 'followhints' is 1 or 2)
459      *
460      */
461     process: function _processHints(followFirst) {
462         dactyl.assert(this.validHints.length > 0);
463
464         // This "followhints" option is *too* confusing. For me, and
465         // presumably for users, too. --Kris
466         if (options["followhints"] > 0 && !followFirst)
467             return; // no return hit; don't examine uniqueness
468
469         if (!followFirst) {
470             let firstHref = this.validHints[0].elem.getAttribute("href") || null;
471             if (firstHref) {
472                 if (this.validHints.some(function (h) h.elem.getAttribute("href") != firstHref))
473                     return;
474             }
475             else if (this.validHints.length > 1)
476                 return;
477         }
478
479         let timeout = followFirst || events.feedingKeys ? 0 : 500;
480         let activeIndex = (this.hintNumber ? this.hintNumber - 1 : 0);
481         let elem = this.validHints[activeIndex].elem;
482         let top = this.top;
483
484         if (this.continue)
485             this._reset();
486         else
487             this.removeHints(timeout);
488
489         let n = 5;
490         (function next() {
491             let hinted = n || this.validHints.some(function (h) h.elem === elem);
492             if (!hinted)
493                 hints.setClass(elem, null);
494             else if (n)
495                 hints.setClass(elem, n % 2);
496             else
497                 hints.setClass(elem, this.validHints[Math.max(0, this.hintNumber - 1)].elem === elem);
498
499             if (n--)
500                 this.timeout(next, 50);
501         }).call(this);
502
503         if (!this.continue) {
504             modes.pop();
505             if (timeout)
506                 modes.push(modes.IGNORE, modes.HINTS);
507         }
508
509         dactyl.trapErrors("action", this.hintMode,
510                           elem, elem.href || elem.src || "",
511                           this.extendedhintCount, top);
512
513         this.timeout(function () {
514             if (modes.main == modes.IGNORE && !this.continue)
515                 modes.pop();
516             commandline.lastEcho = null; // Hack.
517             if (this.continue && this.top)
518                 this.show();
519         }, timeout);
520     },
521
522     /**
523      * Remove all hints from the document, and reset the completions.
524      *
525      * Lingers on the active hint briefly to confirm the selection to the user.
526      *
527      * @param {number} timeout The number of milliseconds before the active
528      *     hint disappears.
529      */
530     removeHints: function _removeHints(timeout) {
531         for (let { doc, start, end } in values(this.docs)) {
532             // Goddamn stupid fucking Gecko 1.x security manager bullshit.
533             try { delete doc.dactylLabels; } catch (e) { doc.dactylLabels = undefined; }
534
535             for (let elem in util.evaluateXPath("//*[@dactyl:highlight='hints']", doc))
536                 elem.parentNode.removeChild(elem);
537             for (let i in util.range(start, end + 1)) {
538                 this.pageHints[i].ambiguous = false;
539                 this.pageHints[i].valid = false;
540             }
541         }
542         styles.system.remove("hint-positions");
543
544         this.reset();
545     },
546
547     reset: function reset() {
548         this.pageHints = [];
549         this.validHints = [];
550         this.docs = [];
551         this.clearTimeout();
552     },
553     _reset: function _reset() {
554         if (!this.usedTabKey)
555             this.hintNumber = 0;
556         if (this.continue && this.validHints.length <= 1) {
557             this.hintString = "";
558             commandline.widgets.command = this.hintString;
559             this.show();
560         }
561         this.updateStatusline();
562     },
563
564     /**
565      * Display the hints in pageHints that are still valid.
566      */
567     show: function _show() {
568         let hintnum = 1;
569         let validHint = hints.hintMatcher(this.hintString.toLowerCase());
570         let activeHint = this.hintNumber || 1;
571         this.validHints = [];
572
573         for (let { doc, start, end } in values(this.docs)) {
574             let [offsetX, offsetY] = this.getContainerOffsets(doc);
575
576         inner:
577             for (let i in (util.interruptibleRange(start, end + 1, 500))) {
578                 let hint = this.pageHints[i];
579
580                 hint.valid = validHint(hint.text);
581                 if (!hint.valid)
582                     continue inner;
583
584                 if (hint.text == "" && hint.elem.firstChild && hint.elem.firstChild instanceof HTMLImageElement) {
585                     if (!hint.imgSpan) {
586                         let rect = hint.elem.firstChild.getBoundingClientRect();
587                         if (!rect)
588                             continue;
589
590                         hint.imgSpan = util.xmlToDom(<span highlight="Hint" dactyl:hl="HintImage" xmlns:dactyl={NS}/>, doc);
591                         hint.imgSpan.style.display = "none";
592                         hint.imgSpan.style.left = (rect.left + offsetX) + "px";
593                         hint.imgSpan.style.top = (rect.top + offsetY) + "px";
594                         hint.imgSpan.style.width = (rect.right - rect.left) + "px";
595                         hint.imgSpan.style.height = (rect.bottom - rect.top) + "px";
596                         hint.span.parentNode.appendChild(hint.imgSpan);
597                     }
598                 }
599
600                 let str = this.getHintString(hintnum);
601                 let text = [];
602                 if (hint.elem instanceof HTMLInputElement)
603                     if (hint.elem.type === "radio")
604                         text.push(UTF8(hint.elem.checked ? "⊙" : "○"));
605                     else if (hint.elem.type === "checkbox")
606                         text.push(UTF8(hint.elem.checked ? "☑" : "☐"));
607                 if (hint.showText && !/^\s*$/.test(hint.text))
608                     text.push(hint.text.substr(0, 50));
609
610                 hint.span.setAttribute("text", str + (text.length ? ": " + text.join(" ") : ""));
611                 hint.span.setAttribute("number", str);
612                 if (hint.imgSpan)
613                     hint.imgSpan.setAttribute("number", str);
614
615                 hint.active = activeHint == hintnum;
616
617                 this.validHints.push(hint);
618                 hintnum++;
619             }
620         }
621
622         let base = this.hintKeys.length;
623         for (let [i, hint] in Iterator(this.validHints))
624             hint.ambiguous = (i + 1) * base <= this.validHints.length;
625
626         if (options["usermode"]) {
627             let css = [];
628             for (let hint in values(this.pageHints)) {
629                 let selector = highlight.selector("Hint") + "[number=" + hint.span.getAttribute("number").quote() + "]";
630                 let imgSpan = "[dactyl|hl=HintImage]";
631                 css.push(selector + ":not(" + imgSpan + ") { " + hint.span.style.cssText + " }");
632                 if (hint.imgSpan)
633                     css.push(selector + imgSpan + " { " + hint.span.style.cssText + " }");
634             }
635             styles.system.add("hint-positions", "*", css.join("\n"));
636         }
637
638         return true;
639     },
640
641     /**
642      * Update the activeHint.
643      *
644      * By default highlights it green instead of yellow.
645      *
646      * @param {number} newId The hint to make active.
647      * @param {number} oldId The currently active hint.
648      */
649     showActiveHint: function _showActiveHint(newId, oldId) {
650         let oldHint = this.validHints[oldId - 1];
651         if (oldHint)
652             oldHint.active = false;
653
654         let newHint = this.validHints[newId - 1];
655         if (newHint)
656             newHint.active = true;
657     },
658
659     backspace: function () {
660         this.clearTimeout();
661         if (this.prevInput !== "number")
662             return Events.PASS;
663
664         if (this.hintNumber > 0 && !this.usedTabKey) {
665             this.hintNumber = Math.floor(this.hintNumber / this.hintKeys.length);
666             if (this.hintNumber == 0)
667                 this.prevInput = "text";
668             this.update(false);
669         }
670         else {
671             this.usedTabKey = false;
672             this.hintNumber = 0;
673             dactyl.beep();
674         }
675         return Events.KILL;
676     },
677
678     updateValidNumbers: function updateValidNumbers(always) {
679         let string = this.getHintString(this.hintNumber);
680         for (let hint in values(this.validHints))
681             hint.valid = always || hint.span.getAttribute("number").indexOf(string) == 0;
682     },
683
684     tab: function tab(previous) {
685         this.clearTimeout();
686         this.usedTabKey = true;
687         if (this.hintNumber == 0)
688             this.hintNumber = 1;
689
690         let oldId = this.hintNumber;
691         if (!previous) {
692             if (++this.hintNumber > this.validHints.length)
693                 this.hintNumber = 1;
694         }
695         else {
696             if (--this.hintNumber < 1)
697                 this.hintNumber = this.validHints.length;
698         }
699
700         this.updateValidNumbers(true);
701         this.showActiveHint(this.hintNumber, oldId);
702         this.updateStatusline();
703     },
704
705     update: function update(followFirst) {
706         this.clearTimeout();
707         this.updateStatusline();
708
709         if (this.docs.length == 0 && this.hintString.length > 0)
710             this.generate();
711
712         this.show();
713         this.process(followFirst);
714     },
715
716     /**
717      * Display the current status to the user.
718      */
719     updateStatusline: function _updateStatusline() {
720         statusline.inputBuffer = (this.escapeNumbers ? options["mapleader"] : "") +
721                                  (this.hintNumber ? this.getHintString(this.hintNumber) : "");
722     },
723 });
724
725 var Hints = Module("hints", {
726     init: function init() {
727         this.resizeTimer = Timer(100, 500, function () {
728             if (isinstance(modes.main, modes.HINTS))
729                 modes.getStack(0).params.onResize();
730         });
731
732         let appContent = document.getElementById("appcontent");
733         if (appContent)
734             events.listen(appContent, "scroll", this.resizeTimer.closure.tell, false);
735
736         const Mode = Hints.Mode;
737         Mode.prototype.__defineGetter__("matcher", function ()
738             options.get("extendedhinttags").getKey(this.name, options.get("hinttags").matcher));
739
740         this.modes = {};
741         this.addMode(";", "Focus hint",                           buffer.closure.focusElement);
742         this.addMode("?", "Show information for hint",            function (elem) buffer.showElementInfo(elem));
743         this.addMode("s", "Save hint",                            function (elem) buffer.saveLink(elem, false));
744         this.addMode("f", "Focus frame",                          function (elem) dactyl.focus(elem.ownerDocument.defaultView));
745         this.addMode("F", "Focus frame or pseudo-frame",          buffer.closure.focusElement, isScrollable);
746         this.addMode("o", "Follow hint",                          function (elem) buffer.followLink(elem, dactyl.CURRENT_TAB));
747         this.addMode("t", "Follow hint in a new tab",             function (elem) buffer.followLink(elem, dactyl.NEW_TAB));
748         this.addMode("b", "Follow hint in a background tab",      function (elem) buffer.followLink(elem, dactyl.NEW_BACKGROUND_TAB));
749         this.addMode("w", "Follow hint in a new window",          function (elem) buffer.followLink(elem, dactyl.NEW_WINDOW));
750         this.addMode("O", "Generate an ‘:open URL’ prompt",       function (elem, loc) CommandExMode().open("open " + loc));
751         this.addMode("T", "Generate a ‘:tabopen URL’ prompt",     function (elem, loc) CommandExMode().open("tabopen " + loc));
752         this.addMode("W", "Generate a ‘:winopen URL’ prompt",     function (elem, loc) CommandExMode().open("winopen " + loc));
753         this.addMode("a", "Add a bookmark",                       function (elem) bookmarks.addSearchKeyword(elem));
754         this.addMode("S", "Add a search keyword",                 function (elem) bookmarks.addSearchKeyword(elem));
755         this.addMode("v", "View hint source",                     function (elem, loc) buffer.viewSource(loc, false));
756         this.addMode("V", "View hint source in external editor",  function (elem, loc) buffer.viewSource(loc, true));
757         this.addMode("y", "Yank hint location",                   function (elem, loc) dactyl.clipboardWrite(loc, true));
758         this.addMode("Y", "Yank hint description",                function (elem) dactyl.clipboardWrite(elem.textContent || "", true));
759         this.addMode("c", "Open context menu",                    function (elem) buffer.openContextMenu(elem));
760         this.addMode("i", "Show image",                           function (elem) dactyl.open(elem.src));
761         this.addMode("I", "Show image in a new tab",              function (elem) dactyl.open(elem.src, dactyl.NEW_TAB));
762
763         function isScrollable(elem) isinstance(elem, [HTMLFrameElement, HTMLIFrameElement]) ||
764             Buffer.isScrollable(elem, 0, true) || Buffer.isScrollable(elem, 0, false);
765     },
766
767     hintSession: Modes.boundProperty(),
768
769     /**
770      * Creates a new hints mode.
771      *
772      * @param {string} mode The letter that identifies this mode.
773      * @param {string} prompt The description to display to the user
774      *     about this mode.
775      * @param {function(Node)} action The function to be called with the
776      *     element that matches.
777      * @param {function(Node):boolean} filter A function used to filter
778      *     the returned node set.
779      * @param {[string]} tags A value to add to the default
780      *     'extendedhinttags' value for this mode.
781      *     @optional
782      */
783     addMode: function (mode, prompt, action, filter, tags) {
784         function toString(regexp) RegExp.prototype.toString.call(regexp);
785
786         if (tags != null) {
787             let eht = options.get("extendedhinttags");
788             let update = eht.isDefault;
789
790             let value = eht.parse(Option.quote(util.regexp.escape(mode)) + ":" + tags.map(Option.quote))[0];
791             eht.defaultValue = eht.defaultValue.filter(function (re) toString(re) != toString(value))
792                                   .concat(value);
793
794             if (update)
795                 eht.reset();
796         }
797
798         this.modes[mode] = Hints.Mode(mode, UTF8(prompt), action, filter);
799     },
800
801     /**
802      * Get a hint for "input", "textarea" and "select".
803      *
804      * Tries to use <label>s if possible but does not try to guess that a
805      * neighboring element might look like a label. Only called by
806      * {@link #_generate}.
807      *
808      * If it finds a hint it returns it, if the hint is not the caption of the
809      * element it will return showText=true.
810      *
811      * @param {Object} elem The element used to generate hint text.
812      * @param {Document} doc The containing document.
813      *
814      * @returns [text, showText]
815      */
816     getInputHint: function _getInputHint(elem, doc) {
817         // <input type="submit|button|reset"/>   Always use the value
818         // <input type="radio|checkbox"/>        Use the value if it is not numeric or label or name
819         // <input type="password"/>              Never use the value, use label or name
820         // <input type="text|file"/> <textarea/> Use value if set or label or name
821         // <input type="image"/>                 Use the alt text if present (showText) or label or name
822         // <input type="hidden"/>                Never gets here
823         // <select/>                             Use the text of the selected item or label or name
824
825         let type = elem.type;
826
827         if (elem instanceof HTMLInputElement && Set.has(util.editableInputs, elem.type))
828             return [elem.value, false];
829         else {
830             for (let [, option] in Iterator(options["hintinputs"])) {
831                 if (option == "value") {
832                     if (elem instanceof HTMLSelectElement) {
833                         if (elem.selectedIndex >= 0)
834                             return [elem.item(elem.selectedIndex).text.toLowerCase(), false];
835                     }
836                     else if (type == "image") {
837                         if (elem.alt)
838                             return [elem.alt.toLowerCase(), true];
839                     }
840                     else if (elem.value && type != "password") {
841                         // radios and checkboxes often use internal ids as values - maybe make this an option too...
842                         if (! ((type == "radio" || type == "checkbox") && !isNaN(elem.value)))
843                             return [elem.value.toLowerCase(), (type == "radio" || type == "checkbox")];
844                     }
845                 }
846                 else if (option == "label") {
847                     if (elem.id) {
848                         let label = elem.ownerDocument.dactylLabels[elem.id];
849                         if (label)
850                             return [label.textContent.toLowerCase(), true];
851                     }
852                 }
853                 else if (option == "name")
854                     return [elem.name.toLowerCase(), true];
855             }
856         }
857
858         return ["", false];
859     },
860
861     /**
862      * Get the hintMatcher according to user preference.
863      *
864      * @param {string} hintString The currently typed hint.
865      * @returns {hintMatcher}
866      */
867     hintMatcher: function _hintMatcher(hintString) { //{{{
868         /**
869          * Divide a string by a regular expression.
870          *
871          * @param {RegExp|string} pat The pattern to split on.
872          * @param {string} str The string to split.
873          * @returns {Array(string)} The lowercased splits of the splitting.
874          */
875         function tokenize(pat, str) str.split(pat).map(String.toLowerCase);
876
877         /**
878          * Get a hint matcher for hintmatching=contains
879          *
880          * The hintMatcher expects the user input to be space delimited and it
881          * returns true if each set of characters typed can be found, in any
882          * order, in the link.
883          *
884          * @param {string} hintString The string typed by the user.
885          * @returns {function(String):boolean} A function that takes the text
886          *     of a hint and returns true if all the (space-delimited) sets of
887          *     characters typed by the user can be found in it.
888          */
889         function containsMatcher(hintString) { //{{{
890             let tokens = tokenize(/\s+/, hintString);
891             return function (linkText) {
892                 linkText = linkText.toLowerCase();
893                 return tokens.every(function (token) indexOf(linkText, token) >= 0);
894             };
895         } //}}}
896
897         /**
898          * Get a hintMatcher for hintmatching=firstletters|wordstartswith
899          *
900          * The hintMatcher will look for any division of the user input that
901          * would match the first letters of words. It will always only match
902          * words in order.
903          *
904          * @param {string} hintString The string typed by the user.
905          * @param {boolean} allowWordOverleaping Whether to allow non-contiguous
906          *     words to match.
907          * @returns {function(String):boolean} A function that will filter only
908          *     hints that match as above.
909          */
910         function wordStartsWithMatcher(hintString, allowWordOverleaping) { //{{{
911             let hintStrings     = tokenize(/\s+/, hintString);
912             let wordSplitRegexp = util.regexp(options["wordseparators"]);
913
914             /**
915              * Match a set of characters to the start of words.
916              *
917              * What the **** does this do? --Kris
918              * This function matches hintStrings like 'hekho' to links
919              * like 'Hey Kris, how are you?' -> [HE]y [K]ris [HO]w are you
920              * --Daniel
921              *
922              * @param {string} chars The characters to match.
923              * @param {Array(string)} words The words to match them against.
924              * @param {boolean} allowWordOverleaping Whether words may be
925              *     skipped during matching.
926              * @returns {boolean} Whether a match can be found.
927              */
928             function charsAtBeginningOfWords(chars, words, allowWordOverleaping) {
929                 function charMatches(charIdx, chars, wordIdx, words, inWordIdx, allowWordOverleaping) {
930                     let matches = (chars[charIdx] == words[wordIdx][inWordIdx]);
931                     if ((matches == false && allowWordOverleaping) || words[wordIdx].length == 0) {
932                         let nextWordIdx = wordIdx + 1;
933                         if (nextWordIdx == words.length)
934                             return false;
935
936                         return charMatches(charIdx, chars, nextWordIdx, words, 0, allowWordOverleaping);
937                     }
938
939                     if (matches) {
940                         let nextCharIdx = charIdx + 1;
941                         if (nextCharIdx == chars.length)
942                             return true;
943
944                         let nextWordIdx = wordIdx + 1;
945                         let beyondLastWord = (nextWordIdx == words.length);
946                         let charMatched = false;
947                         if (beyondLastWord == false)
948                             charMatched = charMatches(nextCharIdx, chars, nextWordIdx, words, 0, allowWordOverleaping);
949
950                         if (charMatched)
951                             return true;
952
953                         if (charMatched == false || beyondLastWord == true) {
954                             let nextInWordIdx = inWordIdx + 1;
955                             if (nextInWordIdx == words[wordIdx].length)
956                                 return false;
957
958                             return charMatches(nextCharIdx, chars, wordIdx, words, nextInWordIdx, allowWordOverleaping);
959                         }
960                     }
961
962                     return false;
963                 }
964
965                 return charMatches(0, chars, 0, words, 0, allowWordOverleaping);
966             }
967
968             /**
969              * Check whether the array of strings all exist at the start of the
970              * words.
971              *
972              * i.e. ['ro', 'e'] would match ['rollover', 'effect']
973              *
974              * The matches must be in order, and, if allowWordOverleaping is
975              * false, contiguous.
976              *
977              * @param {Array(string)} strings The strings to search for.
978              * @param {Array(string)} words The words to search in.
979              * @param {boolean} allowWordOverleaping Whether matches may be
980              *     non-contiguous.
981              * @returns {boolean} Whether all the strings matched.
982              */
983             function stringsAtBeginningOfWords(strings, words, allowWordOverleaping) {
984                 let strIdx = 0;
985                 for (let [, word] in Iterator(words)) {
986                     if (word.length == 0)
987                         continue;
988
989                     let str = strings[strIdx];
990                     if (str.length == 0 || indexOf(word, str) == 0)
991                         strIdx++;
992                     else if (!allowWordOverleaping)
993                         return false;
994
995                     if (strIdx == strings.length)
996                         return true;
997                 }
998
999                 for (; strIdx < strings.length; strIdx++) {
1000                     if (strings[strIdx].length != 0)
1001                         return false;
1002                 }
1003                 return true;
1004             }
1005
1006             return function (linkText) {
1007                 if (hintStrings.length == 1 && hintStrings[0].length == 0)
1008                     return true;
1009
1010                 let words = tokenize(wordSplitRegexp, linkText);
1011                 if (hintStrings.length == 1)
1012                     return charsAtBeginningOfWords(hintStrings[0], words, allowWordOverleaping);
1013                 else
1014                     return stringsAtBeginningOfWords(hintStrings, words, allowWordOverleaping);
1015             };
1016         } //}}}
1017
1018         let indexOf = String.indexOf;
1019         if (options.get("hintmatching").has("transliterated"))
1020             indexOf = Hints.indexOf;
1021
1022         switch (options["hintmatching"][0]) {
1023         case "contains"      : return containsMatcher(hintString);
1024         case "wordstartswith": return wordStartsWithMatcher(hintString, true);
1025         case "firstletters"  : return wordStartsWithMatcher(hintString, false);
1026         case "custom"        : return dactyl.plugins.customHintMatcher(hintString);
1027         default              : dactyl.echoerr(_("hints.noMatcher", hintMatching));
1028         }
1029         return null;
1030     }, //}}}
1031
1032     open: function open(mode, opts) {
1033         this._extendedhintCount = opts.count;
1034         commandline.input(["Normal", mode], "", {
1035             autocomplete: false,
1036             completer: function (context) {
1037                 context.compare = function () 0;
1038                 context.completions = [[k, v.prompt] for ([k, v] in Iterator(hints.modes))];
1039             },
1040             onSubmit: function (arg) {
1041                 if (arg)
1042                     hints.show(arg, opts);
1043             },
1044             onChange: function (arg) {
1045                 if (Object.keys(hints.modes).some(function (m) m != arg && m.indexOf(arg) == 0))
1046                     return;
1047
1048                 this.accepted = true;
1049                 modes.pop();
1050             }
1051         });
1052     },
1053
1054     /**
1055      * Toggle the highlight of a hint.
1056      *
1057      * @param {Object} elem The element to toggle.
1058      * @param {boolean} active Whether it is the currently active hint or not.
1059      */
1060     setClass: function _setClass(elem, active) {
1061         if (elem.dactylHighlight == null)
1062             elem.dactylHighlight = elem.getAttributeNS(NS, "highlight") || "";
1063
1064         let prefix = (elem.getAttributeNS(NS, "hl") || "") + " " + elem.dactylHighlight + " ";
1065         if (active)
1066             highlight.highlightNode(elem, prefix + "HintActive");
1067         else if (active != null)
1068             highlight.highlightNode(elem, prefix + "HintElem");
1069         else {
1070             highlight.highlightNode(elem, elem.dactylHighlight);
1071             // delete elem.dactylHighlight fails on Gecko 1.9. Issue #197
1072             elem.dactylHighlight = null;
1073         }
1074     },
1075
1076     show: function show(mode, opts) {
1077         this.hintSession = HintSession(mode, opts);
1078     }
1079 }, {
1080     translitTable: Class.memoize(function () {
1081         const table = {};
1082         [
1083             [0x00c0, 0x00c6, ["A"]], [0x00c7, 0x00c7, ["C"]],
1084             [0x00c8, 0x00cb, ["E"]], [0x00cc, 0x00cf, ["I"]],
1085             [0x00d1, 0x00d1, ["N"]], [0x00d2, 0x00d6, ["O"]],
1086             [0x00d8, 0x00d8, ["O"]], [0x00d9, 0x00dc, ["U"]],
1087             [0x00dd, 0x00dd, ["Y"]], [0x00e0, 0x00e6, ["a"]],
1088             [0x00e7, 0x00e7, ["c"]], [0x00e8, 0x00eb, ["e"]],
1089             [0x00ec, 0x00ef, ["i"]], [0x00f1, 0x00f1, ["n"]],
1090             [0x00f2, 0x00f6, ["o"]], [0x00f8, 0x00f8, ["o"]],
1091             [0x00f9, 0x00fc, ["u"]], [0x00fd, 0x00fd, ["y"]],
1092             [0x00ff, 0x00ff, ["y"]], [0x0100, 0x0105, ["A", "a"]],
1093             [0x0106, 0x010d, ["C", "c"]], [0x010e, 0x0111, ["D", "d"]],
1094             [0x0112, 0x011b, ["E", "e"]], [0x011c, 0x0123, ["G", "g"]],
1095             [0x0124, 0x0127, ["H", "h"]], [0x0128, 0x0130, ["I", "i"]],
1096             [0x0132, 0x0133, ["IJ", "ij"]], [0x0134, 0x0135, ["J", "j"]],
1097             [0x0136, 0x0136, ["K", "k"]], [0x0139, 0x0142, ["L", "l"]],
1098             [0x0143, 0x0148, ["N", "n"]], [0x0149, 0x0149, ["n"]],
1099             [0x014c, 0x0151, ["O", "o"]], [0x0152, 0x0153, ["OE", "oe"]],
1100             [0x0154, 0x0159, ["R", "r"]], [0x015a, 0x0161, ["S", "s"]],
1101             [0x0162, 0x0167, ["T", "t"]], [0x0168, 0x0173, ["U", "u"]],
1102             [0x0174, 0x0175, ["W", "w"]], [0x0176, 0x0178, ["Y", "y", "Y"]],
1103             [0x0179, 0x017e, ["Z", "z"]], [0x0180, 0x0183, ["b", "B", "B", "b"]],
1104             [0x0187, 0x0188, ["C", "c"]], [0x0189, 0x0189, ["D"]],
1105             [0x018a, 0x0192, ["D", "D", "d", "F", "f"]],
1106             [0x0193, 0x0194, ["G"]],
1107             [0x0197, 0x019b, ["I", "K", "k", "l", "l"]],
1108             [0x019d, 0x01a1, ["N", "n", "O", "O", "o"]],
1109             [0x01a4, 0x01a5, ["P", "p"]], [0x01ab, 0x01ab, ["t"]],
1110             [0x01ac, 0x01b0, ["T", "t", "T", "U", "u"]],
1111             [0x01b2, 0x01d2, ["V", "Y", "y", "Z", "z", "D", "L", "N", "A", "a",
1112                "I", "i", "O", "o"]],
1113             [0x01d3, 0x01dc, ["U", "u"]], [0x01de, 0x01e1, ["A", "a"]],
1114             [0x01e2, 0x01e3, ["AE", "ae"]],
1115             [0x01e4, 0x01ed, ["G", "g", "G", "g", "K", "k", "O", "o", "O", "o"]],
1116             [0x01f0, 0x01f5, ["j", "D", "G", "g"]],
1117             [0x01fa, 0x01fb, ["A", "a"]], [0x01fc, 0x01fd, ["AE", "ae"]],
1118             [0x01fe, 0x0217, ["O", "o", "A", "a", "A", "a", "E", "e", "E", "e",
1119                "I", "i", "I", "i", "O", "o", "O", "o", "R", "r", "R", "r", "U",
1120                "u", "U", "u"]],
1121             [0x0253, 0x0257, ["b", "c", "d", "d"]],
1122             [0x0260, 0x0269, ["g", "h", "h", "i", "i"]],
1123             [0x026b, 0x0273, ["l", "l", "l", "l", "m", "n", "n"]],
1124             [0x027c, 0x028b, ["r", "r", "r", "r", "s", "t", "u", "u", "v"]],
1125             [0x0290, 0x0291, ["z"]], [0x029d, 0x02a0, ["j", "q"]],
1126             [0x1e00, 0x1e09, ["A", "a", "B", "b", "B", "b", "B", "b", "C", "c"]],
1127             [0x1e0a, 0x1e13, ["D", "d"]], [0x1e14, 0x1e1d, ["E", "e"]],
1128             [0x1e1e, 0x1e21, ["F", "f", "G", "g"]], [0x1e22, 0x1e2b, ["H", "h"]],
1129             [0x1e2c, 0x1e8f, ["I", "i", "I", "i", "K", "k", "K", "k", "K", "k",
1130                "L", "l", "L", "l", "L", "l", "L", "l", "M", "m", "M", "m", "M",
1131                "m", "N", "n", "N", "n", "N", "n", "N", "n", "O", "o", "O", "o",
1132                "O", "o", "O", "o", "P", "p", "P", "p", "R", "r", "R", "r", "R",
1133                "r", "R", "r", "S", "s", "S", "s", "S", "s", "S", "s", "S", "s",
1134                "T", "t", "T", "t", "T", "t", "T", "t", "U", "u", "U", "u", "U",
1135                "u", "U", "u", "U", "u", "V", "v", "V", "v", "W", "w", "W", "w",
1136                "W", "w", "W", "w", "W", "w", "X", "x", "X", "x", "Y", "y"]],
1137             [0x1e90, 0x1e9a, ["Z", "z", "Z", "z", "Z", "z", "h", "t", "w", "y", "a"]],
1138             [0x1ea0, 0x1eb7, ["A", "a"]], [0x1eb8, 0x1ec7, ["E", "e"]],
1139             [0x1ec8, 0x1ecb, ["I", "i"]], [0x1ecc, 0x1ee3, ["O", "o"]],
1140             [0x1ee4, 0x1ef1, ["U", "u"]], [0x1ef2, 0x1ef9, ["Y", "y"]],
1141             [0x2071, 0x2071, ["i"]], [0x207f, 0x207f, ["n"]],
1142             [0x249c, 0x24b5, "a"], [0x24b6, 0x24cf, "A"],
1143             [0x24d0, 0x24e9, "a"],
1144             [0xfb00, 0xfb06, ["ff", "fi", "fl", "ffi", "ffl", "st", "st"]],
1145             [0xff21, 0xff3a, "A"], [0xff41, 0xff5a, "a"]
1146         ].forEach(function (start, stop, val) {
1147             if (typeof val != "string")
1148                 for (let i = start; i <= stop; i++)
1149                     table[String.fromCharCode(i)] = val[(i - start) % val.length];
1150             else {
1151                 let n = val.charCodeAt(0);
1152                 for (let i = start; i <= stop; i++)
1153                     table[String.fromCharCode(i)] = String.fromCharCode(n + i - start);
1154             }
1155         });
1156         return table;
1157     }),
1158     indexOf: function indexOf(dest, src) {
1159         let table = this.translitTable;
1160         var end = dest.length - src.length;
1161         if (src.length == 0)
1162             return 0;
1163     outer:
1164         for (var i = 0; i < end; i++) {
1165                 var j = i;
1166                 for (var k = 0; k < src.length;) {
1167                     var s = dest[j++];
1168                     s = table[s] || s;
1169                     for (var l = 0; l < s.length; l++, k++) {
1170                         if (s[l] != src[k])
1171                             continue outer;
1172                         if (k == src.length - 1)
1173                             return i;
1174                     }
1175                 }
1176             }
1177         return -1;
1178     },
1179
1180     Mode: Struct("HintMode", "name", "prompt", "action", "filter")
1181             .localize("prompt")
1182 }, {
1183     modes: function initModes() {
1184         initModes.require("commandline");
1185         modes.addMode("HINTS", {
1186             extended: true,
1187             description: "Active when selecting elements with hints",
1188             bases: [modes.COMMAND_LINE],
1189             input: true,
1190             ownsBuffer: true
1191         });
1192     },
1193     mappings: function () {
1194         var myModes = config.browserModes.concat(modes.OUTPUT_MULTILINE);
1195         mappings.add(myModes, ["f"],
1196             "Start Hints mode",
1197             function () { hints.show("o"); });
1198
1199         mappings.add(myModes, ["F"],
1200             "Start Hints mode, but open link in a new tab",
1201             function () { hints.show(options.get("activate").has("links") ? "t" : "b"); });
1202
1203         mappings.add(myModes, [";"],
1204             "Start an extended hints mode",
1205             function ({ count }) { hints.open(";", { count: count }); },
1206             { count: true });
1207
1208         mappings.add(myModes, ["g;"],
1209             "Start an extended hints mode and stay there until <Esc> is pressed",
1210             function ({ count }) { hints.open("g;", { continue: true, count: count }); },
1211             { count: true });
1212
1213         mappings.add(modes.HINTS, ["<Return>"],
1214             "Follow the selected hint",
1215             function ({ self }) { self.update(true); });
1216
1217         mappings.add(modes.HINTS, ["<Tab>"],
1218             "Focus the next matching hint",
1219             function ({ self }) { self.tab(false); });
1220
1221         mappings.add(modes.HINTS, ["<S-Tab>"],
1222             "Focus the previous matching hint",
1223             function ({ self }) { self.tab(true); });
1224
1225         mappings.add(modes.HINTS, ["<BS>", "<C-h>"],
1226             "Delete the previous character",
1227             function ({ self }) self.backspace());
1228
1229         mappings.add(modes.HINTS, ["<Leader>"],
1230             "Toggle hint filtering",
1231             function ({ self }) { self.escapeNumbers = !self.escapeNumbers; });
1232     },
1233     options: function () {
1234         function xpath(arg) util.makeXPath(arg);
1235
1236         options.add(["extendedhinttags", "eht"],
1237             "XPath or CSS selector strings of hintable elements for extended hint modes",
1238             "regexpmap", {
1239                 "[iI]": "img",
1240                 "[asOTvVWy]": ["a[href]", "area[href]", "img[src]", "iframe[src]"],
1241                 "[f]": "body",
1242                 "[F]": ["body", "code", "div", "html", "p", "pre", "span"],
1243                 "[S]": ["input:not([type=hidden])", "textarea", "button", "select"]
1244             },
1245             {
1246                 keepQuotes: true,
1247                 getKey: function (val, default_)
1248                     let (res = array.nth(this.value, function (re) let (match = re.exec(val)) match && match[0] == val, 0))
1249                         res ? res.matcher : default_,
1250                 setter: function (vals) {
1251                     for (let value in values(vals))
1252                         value.matcher = util.compileMatcher(Option.splitList(value.result));
1253                     return vals;
1254                 },
1255                 validator: util.validateMatcher
1256             });
1257
1258         options.add(["hinttags", "ht"],
1259             "XPath or CSS selector strings of hintable elements for Hints mode",
1260             "stringlist", "input:not([type=hidden]),a[href],area,iframe,textarea,button,select," +
1261                           "[onclick],[onmouseover],[onmousedown],[onmouseup],[oncommand]," +
1262                           "[tabindex],[role=link],[role=button],[contenteditable=true]",
1263             {
1264                 setter: function (values) {
1265                     this.matcher = util.compileMatcher(values);
1266                     return values;
1267                 },
1268                 validator: util.validateMatcher
1269             });
1270
1271         options.add(["hintkeys", "hk"],
1272             "The keys used to label and select hints",
1273             "string", "0123456789",
1274             {
1275                 values: {
1276                     "0123456789": "Numbers",
1277                     "asdfg;lkjh": "Home Row"
1278                 },
1279                 validator: function (value) {
1280                     let values = events.fromString(value).map(events.closure.toString);
1281                     return Option.validIf(array.uniq(values).length === values.length && values.length > 1,
1282                                           _("option.hintkeys.duplicate"));
1283                 }
1284             });
1285
1286         options.add(["hinttimeout", "hto"],
1287             "Timeout before automatically following a non-unique numerical hint",
1288             "number", 0,
1289             { validator: function (value) value >= 0 });
1290
1291         options.add(["followhints", "fh"],
1292             "Define the conditions under which selected hints are followed",
1293             "number", 0,
1294             {
1295                 values: {
1296                     "0": "Follow the first hint as soon as typed text uniquely identifies it. Follow the selected hint on <Return>.",
1297                     "1": "Follow the selected hint on <Return>.",
1298                 }
1299             });
1300
1301         options.add(["hintmatching", "hm"],
1302             "How hints are filtered",
1303             "stringlist", "contains",
1304             {
1305                 values: {
1306                     "contains":       "The typed characters are split on whitespace. The resulting groups must all appear in the hint.",
1307                     "custom":         "Delegate to a custom function: dactyl.plugins.customHintMatcher(hintString)",
1308                     "firstletters":   "Behaves like wordstartswith, but all groups must match a sequence of words.",
1309                     "wordstartswith": "The typed characters are split on whitespace. The resulting groups must all match the beginnings of words, in order.",
1310                     "transliterated": UTF8("When true, special latin characters are translated to their ASCII equivalents (e.g., é ⇒ e)")
1311                 },
1312                 validator: function (values) Option.validateCompleter.call(this, values) &&
1313                     1 === values.reduce(function (acc, v) acc + (["contains", "custom", "firstletters", "wordstartswith"].indexOf(v) >= 0), 0)
1314             });
1315
1316         options.add(["wordseparators", "wsp"],
1317             "Regular expression defining which characters separate words when matching hints",
1318             "string", '[.,!?:;/"^$%&?()[\\]{}<>#*+|=~ _-]',
1319             { validator: function (value) RegExp(value) });
1320
1321         options.add(["hintinputs", "hin"],
1322             "Which text is used to filter hints for input elements",
1323             "stringlist", "label,value",
1324             {
1325                 values: {
1326                     "value": "Match against the value of the input field",
1327                     "label": "Match against the text of a label for the input field, if one can be found",
1328                     "name":  "Match against the name of the input field"
1329                 }
1330             });
1331     }
1332 });
1333
1334 // vim: set fdm=marker sw=4 ts=4 et: