]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/template.jsm
Imported Upstream version 1.1+hg7904
[dactyl.git] / common / modules / template.jsm
1 // Copyright (c) 2008-2014 Kris Maglione <maglione.k at Gmail>
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 let global = this;
8 defineModule("template", {
9     exports: ["Binding", "Template", "template"],
10     require: ["util"]
11 });
12
13 lazyRequire("help", ["help"]);
14
15 var Binding = Class("Binding", {
16     init: function (node, nodes) {
17         this.node = node;
18         this.nodes = nodes;
19         node.dactylBinding = this;
20
21         Object.defineProperties(node, this.constructor.properties);
22
23         for (let [event, handler] in values(this.constructor.events))
24             node.addEventListener(event, util.wrapCallback(handler, true), false);
25     },
26
27     set collapsed(collapsed) {
28         if (collapsed)
29             this.setAttribute("collapsed", "true");
30         else
31             this.removeAttribute("collapsed");
32     },
33     get collapsed() !!this.getAttribute("collapsed"),
34
35     __noSuchMethod__: Class.Property({
36         configurable: true,
37         writeable: true,
38         value: function __noSuchMethod__(meth, args) {
39             return this.node[meth].apply(this.node, args);
40         }
41     })
42 }, {
43     get bindings() {
44         let bindingProto = Object.getPrototypeOf(Binding.prototype);
45         for (let obj = this.prototype; obj !== bindingProto; obj = Object.getPrototypeOf(obj))
46             yield obj;
47     },
48
49     bind: function bind(func) function bound() {
50         try {
51             return func.apply(this.dactylBinding, arguments);
52         }
53         catch (e) {
54             util.reportError(e);
55             throw e;
56         }
57     },
58
59     events: Class.Memoize(function () {
60         let res = [];
61         for (let obj in this.bindings)
62             if (Object.getOwnPropertyDescriptor(obj, "events"))
63                 for (let [event, handler] in Iterator(obj.events))
64                     res.push([event, this.bind(handler)]);
65         return res;
66     }),
67
68     properties: Class.Memoize(function () {
69         let res = {};
70         for (let obj in this.bindings)
71             for (let prop in properties(obj)) {
72                 let desc = Object.getOwnPropertyDescriptor(obj, prop);
73                 if (desc.enumerable) {
74                     for (let k in values(["get", "set", "value"]))
75                         if (typeof desc[k] === "function")
76                             desc[k] = this.bind(desc[k]);
77                     res[prop] = desc;
78                 }
79             }
80         return res;
81     })
82 });
83
84 ["appendChild", "getAttribute", "insertBefore", "setAttribute"].forEach(function (key) {
85     Object.defineProperty(Binding.prototype, key, {
86         configurable: true,
87         enumerable: false,
88         value: function () this.node[key].apply(this.node, arguments),
89         writable: true
90     });
91 });
92
93 var Template = Module("Template", {
94
95     bindings: {
96         Button: Class("Button", Binding, {
97             init: function init(node, params) {
98                 init.supercall(this, node);
99
100                 this.target = params.commandTarget;
101             },
102
103             get command() this.getAttribute("command") || this.getAttribute("key"),
104
105             events: {
106                 "click": function onClick(event) {
107                     event.preventDefault();
108                     if (this.commandAllowed) {
109                         if (hasOwnProperty(this.target.commands || {}, this.command))
110                             this.target.commands[this.command].call(this.target);
111                         else
112                             this.target.command(this.command);
113                     }
114                 }
115             },
116
117             get commandAllowed() {
118                 if (hasOwnProperty(this.target.allowedCommands || {}, this.command))
119                     return this.target.allowedCommands[this.command];
120                 if ("commandAllowed" in this.target)
121                     return this.target.commandAllowed(this.command);
122                 return true;
123             },
124
125             update: function update() {
126                 let collapsed = this.collapsed;
127                 this.collapsed = !this.commandAllowed;
128
129                 if (collapsed == this.commandAllowed) {
130                     let event = this.node.ownerDocument.createEvent("Events");
131                     event.initEvent("dactyl-commandupdate", true, false);
132                     this.node.ownerDocument.dispatchEvent(event);
133                 }
134             }
135         }),
136
137         Events: Class("Events", Binding, {
138             init: function init(node, params) {
139                 init.supercall(this, node);
140
141                 let obj = params.eventTarget;
142                 let events = obj[this.getAttribute("events") || "events"];
143                 if (hasOwnProperty(events, "input"))
144                     events["dactyl-input"] = events["input"];
145
146                 for (let [event, handler] in Iterator(events))
147                     node.addEventListener(event, util.wrapCallback(handler.bind(obj), true), false);
148             }
149         })
150     },
151
152     map: function map(iter, func, sep, interruptable) {
153         if (typeof iter.length == "number") // FIXME: Kludge?
154             iter = array.iterValues(iter);
155
156         let res = [];
157         let n = 0;
158         for (let i in Iterator(iter)) {
159             let val = func(i, n);
160             if (val == undefined)
161                 continue;
162             if (n++ && sep)
163                 res.push(sep);
164             if (interruptable && n % interruptable == 0)
165                 util.threadYield(true, true);
166             res.push(val);
167         }
168         return res;
169     },
170
171     bookmarkDescription: function (item, text) [
172         !(item.extra && item.extra.length) ? [] :
173         ["span", { highlight: "URLExtra" },
174             " (",
175             template.map(item.extra, e =>
176                 ["", e[0], ": ",
177                  ["span", { highlight: e[2] }, e[1]]],
178                 "\u00a0"),
179             ")\u00a0"],
180         ["a", { identifier: item.id == null ? "" : item.id,
181                 "dactyl:command": item.command || "",
182                 href: item.item.url, highlight: "URL" },
183             text || ""]
184     ],
185
186     filter: function (str) ["span", { highlight: "Filter" }, str],
187
188     completionRow: function completionRow(item, highlightGroup) {
189         if (typeof icon == "function")
190             icon = icon();
191
192         if (highlightGroup) {
193             var text = item[0] || "";
194             var desc = item[1] || "";
195         }
196         else {
197             var text = this.processor[0].call(this, item, item.result);
198             var desc = this.processor[1].call(this, item, item.description);
199         }
200
201         return ["div", { highlight: highlightGroup || "CompItem", style: "white-space: nowrap" },
202                    /* The non-breaking spaces prevent empty elements
203                     * from pushing the baseline down and enlarging
204                     * the row.
205                     */
206                    ["li", { highlight: "CompResult " + item.highlight },
207                        text, "\u00a0"],
208                    ["li", { highlight: "CompDesc" },
209                        desc, "\u00a0"]];
210     },
211
212     helpLink: function (token, text, type) {
213         if (!help.initialized)
214             util.dactyl.initHelp();
215
216         let topic = token; // FIXME: Evil duplication!
217         if (/^\[.*\]$/.test(topic))
218             topic = topic.slice(1, -1);
219         else if (/^n_/.test(topic))
220             topic = topic.slice(2);
221
222         if (help.initialized && !hasOwnProperty(help.tags, topic))
223             return ["span", { highlight: type || ""}, text || token];
224
225         type = type || (/^'.*'$/.test(token)   ? "HelpOpt" :
226                         /^\[.*\]$|^E\d{3}$/.test(token) ? "HelpTopic" :
227                         /^:\w/.test(token)     ? "HelpEx"  : "HelpKey");
228
229         return ["a", { highlight: "InlineHelpLink " + type, tag: topic,
230                        href: "dactyl://help-tag/" + topic,
231                        "dactyl:command": "dactyl.help" },
232                     text || topic];
233     },
234     HelpLink: function (token) {
235         if (!help.initialized)
236             util.dactyl.initHelp();
237
238         let topic = token; // FIXME: Evil duplication!
239         if (/^\[.*\]$/.test(topic))
240             topic = topic.slice(1, -1);
241         else if (/^n_/.test(topic))
242             topic = topic.slice(2);
243
244         if (help.initialized && !hasOwnProperty(help.tags, topic))
245             return token;
246
247         let tag = (/^'.*'$/.test(token)            ? "o" :
248                    /^\[.*\]$|^E\d{3}$/.test(token) ? "t" :
249                    /^:\w/.test(token)              ? "ex"  : "k");
250
251         topic = topic.replace(/^'(.*)'$/, "$1");
252         return [tag, { xmlns: "dactyl" }, topic];
253     },
254     linkifyHelp: function linkifyHelp(str, help) {
255         let re = util.regexp(literal(/*
256             (?P<pre> [/\s]|^)
257             (?P<tag> '[\w-]+' | :(?:[\w-]+!?|!) | (?:._)?<[\w-]+>\w* | \b[a-zA-Z]_(?:[\w[\]]+|.) | \[[\w-;]+\] | E\d{3} )
258             (?=      [[\)!,:;./\s]|$)
259         */), "gx");
260         return this.highlightSubstrings(str, (function () {
261             for (let res in re.iterate(str))
262                 yield [res.index + res.pre.length, res.tag.length];
263         })(), this[help ? "HelpLink" : "helpLink"]);
264     },
265
266     // if "processStrings" is true, any passed strings will be surrounded by " and
267     // any line breaks are displayed as \n
268     highlight: function highlight(arg, processStrings, clip, bw) {
269         // some objects like window.JSON or getBrowsers()._browsers need the try/catch
270         try {
271             let str = String(arg);
272             if (clip)
273                 str = util.clip(str, clip);
274             switch (arg == null ? "undefined" : typeof arg) {
275             case "number":
276                 return ["span", { highlight: "Number" }, str];
277             case "string":
278                 if (processStrings)
279                     str = str.quote();
280                 return ["span", { highlight: "String" }, str];
281             case "boolean":
282                 return ["span", { highlight: "Boolean" }, str];
283             case "function":
284                 if (arg instanceof Ci.nsIDOMElement) // wtf?
285                     return util.objectToString(arg, !bw);
286
287                 str = str.replace("/* use strict */ \n", "/* use strict */ ");
288                 if (processStrings)
289                     return ["span", { highlight: "Function" },
290                                 str.replace(/\{(.|\n)*(?:)/g, "{ ... }")];
291                 arg = String(arg).replace("/* use strict */ \n", "/* use strict */ ");
292                 return arg;
293             case "undefined":
294                 return ["span", { highlight: "Null" }, "undefined"];
295             case "object":
296                 if (arg instanceof Ci.nsIDOMElement)
297                     return util.objectToString(arg, !bw);
298                 if (arg instanceof util.Magic)
299                     return String(arg);
300
301                 if (processStrings && false)
302                     str = template._highlightFilter(str, "\n",
303                                                     function () ["span", { highlight: "NonText" },
304                                                                      "^J"]);
305                 return ["span", { highlight: "Object" }, str];
306             case "xml":
307                 return arg;
308             default:
309                 return "<unknown type>";
310             }
311         }
312         catch (e) {
313             return "<error: " + e + ">";
314         }
315     },
316
317     highlightFilter: function highlightFilter(str, filter, highlight, isURI) {
318         if (isURI)
319             str = util.losslessDecodeURI(str);
320
321         return this.highlightSubstrings(str, (function () {
322             if (filter.length == 0)
323                 return;
324
325             let lcstr = String.toLowerCase(str);
326             let lcfilter = filter.toLowerCase();
327             let start = 0;
328             while ((start = lcstr.indexOf(lcfilter, start)) > -1) {
329                 yield [start, filter.length];
330                 start += filter.length;
331             }
332         })(), highlight || template.filter);
333     },
334
335     highlightRegexp: function highlightRegexp(str, re, highlight) {
336         return this.highlightSubstrings(str, (function () {
337             for (let res in util.regexp.iterate(re, str))
338                 yield [res.index, res[0].length, res.wholeMatch ? [res] : res];
339         })(), highlight || template.filter);
340     },
341
342     highlightSubstrings: function highlightSubstrings(str, iter, highlight) {
343         if (!isString(str))
344             return str;
345
346         if (str == "")
347             return DOM.DOMString(str);
348
349         let s = [""];
350         let start = 0;
351         let n = 0, _i;
352         for (let [i, length, args] in iter) {
353             if (i == _i || i < _i)
354                 break;
355             _i = i;
356
357             s.push(str.substring(start, i),
358                    highlight.apply(this, Array.concat(args || str.substr(i, length))));
359             start = i + length;
360         }
361         s.push(str.substr(start));
362         return s;
363     },
364
365     highlightURL: function highlightURL(str, force) {
366         if (force || /^[a-zA-Z]+:\/\//.test(str))
367             return ["a", { highlight: "URL", href: str },
368                         util.losslessDecodeURI(str)];
369         else
370             return str;
371     },
372
373     icon: function (item, text) [
374         ["span", { highlight: "CompIcon" },
375             item.icon ? ["img", { src: item.icon }] : []],
376         ["span", { class: "td-strut" }],
377         text
378     ],
379
380     jumps: function jumps(index, elems) {
381         return ["table", {},
382                 ["tr", { style: "text-align: left;", highlight: "Title" },
383                     ["th", { colspan: "2" }, _("title.Jump")],
384                     ["th", {}, _("title.HPos")],
385                     ["th", {}, _("title.VPos")],
386                     ["th", {}, _("title.Title")],
387                     ["th", {}, _("title.URI")]],
388                 this.map(Iterator(elems), ([idx, val]) =>
389                     ["tr", {},
390                         ["td", { class: "indicator" }, idx == index ? ">" : ""],
391                         ["td", {}, Math.abs(idx - index)],
392                         ["td", {}, val.offset ? val.offset.x : ""],
393                         ["td", {}, val.offset ? val.offset.y : ""],
394                         ["td", { style: "width: 250px; max-width: 500px; overflow: hidden;" }, val.title],
395                         ["td", {},
396                             ["a", { href: val.URI.spec, highlight: "URL jump-list" },
397                                 util.losslessDecodeURI(val.URI.spec)]]])];
398     },
399
400     options: function options(title, opts, verbose) {
401         return ["table", {},
402                 ["tr", { highlight: "Title", align: "left" },
403                     ["th", {}, "--- " + title + " ---"]],
404                 this.map(opts, opt =>
405                     ["tr", {},
406                         ["td", {},
407                             ["div", { highlight: "Message" },
408                                 ["span", { style: opt.isDefault ? "" : "font-weight: bold" },
409                                     opt.pre, opt.name],
410                                 ["span", {}, opt.value],
411                                 opt.isDefault || opt.default == null ? "" : ["span", { class: "extra-info" }, " (default: ", opt.default, ")"]],
412                             verbose && opt.setFrom ? ["div", { highlight: "Message" },
413                                                          "       Last set from ",
414                                                          template.sourceLink(opt.setFrom)] : ""]])];
415     },
416
417     sourceLink: function (frame) {
418         let url = util.fixURI(frame.filename || "unknown");
419         let path = util.urlPath(url);
420
421         return ["a", { "dactyl:command": "buffer.viewSource",
422                         href: url, path: path, line: frame.lineNumber,
423                         highlight: "URL" },
424             path + ":" + frame.lineNumber];
425     },
426
427     table: function table(title, data, indent) {
428         let table = ["table", {},
429             ["tr", { highlight: "Title", align: "left" },
430                 ["th", { colspan: "2" }, title]],
431             this.map(data, datum =>
432                 ["tr", {},
433                     ["td", { style: "font-weight: bold; min-width: 150px; padding-left: " + (indent || "2ex") }, datum[0]],
434                     ["td", {}, datum[1]]])];
435
436         if (table[3].length)
437             return table;
438     },
439
440     tabular: function tabular(headings, style, iter) {
441         // TODO: This might be mind-bogglingly slow. We'll see.
442         return ["table", {},
443             ["tr", { highlight: "Title", align: "left" },
444                 this.map(headings, function (h)
445                     ["th", {}, h])],
446             this.map(iter, (row) =>
447                 ["tr", {},
448                     this.map(Iterator(row), ([i, d]) =>
449                         ["td", { style: style[i] || "" }, d])])];
450     },
451
452     usage: function usage(iter, format={}) {
453         let desc = format.description || (item => this.linkifyHelp(item.description));
454         let help = format.help || (item => item.name);
455         let sourceLink = (frame) => {
456             let source = this.sourceLink(frame);
457             source[1]["dactyl:hint"] = source[2];
458             return source;
459         }
460         return ["table", {},
461             format.headings ?
462                 ["thead", { highlight: "UsageHead" },
463                     ["tr", { highlight: "Title", align: "left" },
464                         this.map(format.headings, (h) => ["th", {}, h])]] :
465                 [],
466             format.columns ?
467                 ["colgroup", {},
468                     this.map(format.columns, (c) => ["col", { style: c }])] :
469                 [],
470             ["tbody", { highlight: "UsageBody" },
471                 this.map(iter, (item) =>
472                     // Urgh.
473                     let (name = item.name || item.names[0], frame = item.definedAt)
474                         ["tr", { highlight: "UsageItem" },
475                             ["td", { style: "padding-right: 2em;" },
476                                 ["span", { highlight: "Usage Link" },
477                                     !frame ? name :
478                                         [this.helpLink(help(item), name, "Title"),
479                                          ["span", { highlight: "LinkInfo" },
480                                             _("io.definedAt"), " ",
481                                             sourceLink(frame)]]]],
482                             item.columns ? this.map(item.columns, (c) => ["td", {}, c]) : [],
483                             ["td", {}, desc(item)]])]];
484     }
485 });
486
487 endModule();
488
489 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: