]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/template.jsm
5b89f187524e336eb95e7c2899c01a1c686d996d
[dactyl.git] / common / modules / template.jsm
1 // Copyright (c) 2008-2013 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 (Set.has(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 (Set.has(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 (Set.has(events, "input"))
144                     events["dactyl-input"] = events["input"];
145
146                 for (let [event, handler] in Iterator(events))
147                     node.addEventListener(event, util.wrapCallback(obj.closure(handler), 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 each (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 && !Set.has(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 && !Set.has(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     // Fixes some strange stack rewinds on NS_ERROR_OUT_OF_MEMORY
267     // exceptions that we can't catch.
268     stringify: function stringify(arg) {
269         if (!callable(arg))
270             return String(arg);
271
272         try {
273             this._sandbox.arg = arg;
274             return Cu.evalInSandbox("String(arg)", this._sandbox);
275         }
276         finally {
277             this._sandbox.arg = null;
278         }
279     },
280
281     _sandbox: Class.Memoize(() => Cu.Sandbox(Cu.getGlobalForObject(global),
282                                              { wantXrays: false })),
283
284     // if "processStrings" is true, any passed strings will be surrounded by " and
285     // any line breaks are displayed as \n
286     highlight: function highlight(arg, processStrings, clip, bw) {
287         // some objects like window.JSON or getBrowsers()._browsers need the try/catch
288         try {
289             let str = this.stringify(arg);
290             if (clip)
291                 str = util.clip(str, clip);
292             switch (arg == null ? "undefined" : typeof arg) {
293             case "number":
294                 return ["span", { highlight: "Number" }, str];
295             case "string":
296                 if (processStrings)
297                     str = str.quote();
298                 return ["span", { highlight: "String" }, str];
299             case "boolean":
300                 return ["span", { highlight: "Boolean" }, str];
301             case "function":
302                 if (arg instanceof Ci.nsIDOMElement) // wtf?
303                     return util.objectToString(arg, !bw);
304
305                 str = str.replace("/* use strict */ \n", "/* use strict */ ");
306                 if (processStrings)
307                     return ["span", { highlight: "Function" },
308                                 str.replace(/\{(.|\n)*(?:)/g, "{ ... }")];
309                 arg = String(arg).replace("/* use strict */ \n", "/* use strict */ ");
310                 return arg;
311             case "undefined":
312                 return ["span", { highlight: "Null" }, "undefined"];
313             case "object":
314                 if (arg instanceof Ci.nsIDOMElement)
315                     return util.objectToString(arg, !bw);
316                 if (arg instanceof util.Magic)
317                     return String(arg);
318
319                 if (processStrings && false)
320                     str = template._highlightFilter(str, "\n",
321                                                     function () ["span", { highlight: "NonText" },
322                                                                      "^J"]);
323                 return ["span", { highlight: "Object" }, str];
324             case "xml":
325                 return arg;
326             default:
327                 return "<unknown type>";
328             }
329         }
330         catch (e) {
331             return "<error: " + e + ">";
332         }
333     },
334
335     highlightFilter: function highlightFilter(str, filter, highlight, isURI) {
336         if (isURI)
337             str = util.losslessDecodeURI(str);
338
339         return this.highlightSubstrings(str, (function () {
340             if (filter.length == 0)
341                 return;
342
343             let lcstr = String.toLowerCase(str);
344             let lcfilter = filter.toLowerCase();
345             let start = 0;
346             while ((start = lcstr.indexOf(lcfilter, start)) > -1) {
347                 yield [start, filter.length];
348                 start += filter.length;
349             }
350         })(), highlight || template.filter);
351     },
352
353     highlightRegexp: function highlightRegexp(str, re, highlight) {
354         return this.highlightSubstrings(str, (function () {
355             for (let res in util.regexp.iterate(re, str))
356                 yield [res.index, res[0].length, res.wholeMatch ? [res] : res];
357         })(), highlight || template.filter);
358     },
359
360     highlightSubstrings: function highlightSubstrings(str, iter, highlight) {
361         if (!isString(str))
362             return str;
363
364         if (str == "")
365             return DOM.DOMString(str);
366
367         let s = [""];
368         let start = 0;
369         let n = 0, _i;
370         for (let [i, length, args] in iter) {
371             if (i == _i || i < _i)
372                 break;
373             _i = i;
374
375             s.push(str.substring(start, i),
376                    highlight.apply(this, Array.concat(args || str.substr(i, length))));
377             start = i + length;
378         }
379         s.push(str.substr(start));
380         return s;
381     },
382
383     highlightURL: function highlightURL(str, force) {
384         if (force || /^[a-zA-Z]+:\/\//.test(str))
385             return ["a", { highlight: "URL", href: str },
386                         util.losslessDecodeURI(str)];
387         else
388             return str;
389     },
390
391     icon: function (item, text) [
392         ["span", { highlight: "CompIcon" },
393             item.icon ? ["img", { src: item.icon }] : []],
394         ["span", { class: "td-strut" }],
395         text
396     ],
397
398     jumps: function jumps(index, elems) {
399         return ["table", {},
400                 ["tr", { style: "text-align: left;", highlight: "Title" },
401                     ["th", { colspan: "2" }, _("title.Jump")],
402                     ["th", {}, _("title.HPos")],
403                     ["th", {}, _("title.VPos")],
404                     ["th", {}, _("title.Title")],
405                     ["th", {}, _("title.URI")]],
406                 this.map(Iterator(elems), ([idx, val]) =>
407                     ["tr", {},
408                         ["td", { class: "indicator" }, idx == index ? ">" : ""],
409                         ["td", {}, Math.abs(idx - index)],
410                         ["td", {}, val.offset ? val.offset.x : ""],
411                         ["td", {}, val.offset ? val.offset.y : ""],
412                         ["td", { style: "width: 250px; max-width: 500px; overflow: hidden;" }, val.title],
413                         ["td", {},
414                             ["a", { href: val.URI.spec, highlight: "URL jump-list" },
415                                 util.losslessDecodeURI(val.URI.spec)]]])];
416     },
417
418     options: function options(title, opts, verbose) {
419         return ["table", {},
420                 ["tr", { highlight: "Title", align: "left" },
421                     ["th", {}, "--- " + title + " ---"]],
422                 this.map(opts, opt =>
423                     ["tr", {},
424                         ["td", {},
425                             ["div", { highlight: "Message" },
426                                 ["span", { style: opt.isDefault ? "" : "font-weight: bold" },
427                                     opt.pre, opt.name],
428                                 ["span", {}, opt.value],
429                                 opt.isDefault || opt.default == null ? "" : ["span", { class: "extra-info" }, " (default: ", opt.default, ")"]],
430                             verbose && opt.setFrom ? ["div", { highlight: "Message" },
431                                                          "       Last set from ",
432                                                          template.sourceLink(opt.setFrom)] : ""]])];
433     },
434
435     sourceLink: function (frame) {
436         let url = util.fixURI(frame.filename || "unknown");
437         let path = util.urlPath(url);
438
439         return ["a", { "dactyl:command": "buffer.viewSource",
440                         href: url, path: path, line: frame.lineNumber,
441                         highlight: "URL" },
442             path + ":" + frame.lineNumber];
443     },
444
445     table: function table(title, data, indent) {
446         let table = ["table", {},
447             ["tr", { highlight: "Title", align: "left" },
448                 ["th", { colspan: "2" }, title]],
449             this.map(data, datum =>
450                 ["tr", {},
451                     ["td", { style: "font-weight: bold; min-width: 150px; padding-left: " + (indent || "2ex") }, datum[0]],
452                     ["td", {}, datum[1]]])];
453
454         if (table[3].length)
455             return table;
456     },
457
458     tabular: function tabular(headings, style, iter) {
459         // TODO: This might be mind-bogglingly slow. We'll see.
460         return ["table", {},
461             ["tr", { highlight: "Title", align: "left" },
462                 this.map(headings, function (h)
463                     ["th", {}, h])],
464             this.map(iter, (row) =>
465                 ["tr", {},
466                     this.map(Iterator(row), ([i, d]) =>
467                         ["td", { style: style[i] || "" }, d])])];
468     },
469
470     usage: function usage(iter, format = {}) {
471         let desc = format.description || (item => this.linkifyHelp(item.description));
472         let help = format.help || (item => item.name);
473         let sourceLink = (frame) => {
474             let source = this.sourceLink(frame);
475             source[1]["dactyl:hint"] = source[2];
476             return source;
477         }
478         return ["table", {},
479             format.headings ?
480                 ["thead", { highlight: "UsageHead" },
481                     ["tr", { highlight: "Title", align: "left" },
482                         this.map(format.headings, (h) => ["th", {}, h])]] :
483                 [],
484             format.columns ?
485                 ["colgroup", {},
486                     this.map(format.columns, (c) => ["col", { style: c }])] :
487                 [],
488             ["tbody", { highlight: "UsageBody" },
489                 this.map(iter, (item) =>
490                     // Urgh.
491                     let (name = item.name || item.names[0], frame = item.definedAt)
492                         ["tr", { highlight: "UsageItem" },
493                             ["td", { style: "padding-right: 2em;" },
494                                 ["span", { highlight: "Usage Link" },
495                                     !frame ? name :
496                                         [this.helpLink(help(item), name, "Title"),
497                                          ["span", { highlight: "LinkInfo" },
498                                             _("io.definedAt"), " ",
499                                             sourceLink(frame)]]]],
500                             item.columns ? this.map(item.columns, (c) => ["td", {}, c]) : [],
501                             ["td", {}, desc(item)]])]];
502     }
503 });
504
505 endModule();
506
507 // vim: set fdm=marker sw=4 sts=4 ts=8 et ft=javascript: