]> git.donarmstrong.com Git - dactyl.git/blob - common/modules/template.jsm
47fd94db2214804890fd76f9f85f26bdd788570b
[dactyl.git] / common / modules / template.jsm
1 // Copyright (c) 2008-2012 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
172     bookmarkDescription: function (item, text) [
173         !(item.extra && item.extra.length) ? [] :
174         ["span", { highlight: "URLExtra" },
175             " (",
176             template.map(item.extra, function (e)
177                 ["", e[0], ": ",
178                  ["span", { highlight: e[2] }, e[1]]],
179                 "\u00a0"),
180             ")\u00a0"],
181         ["a", { identifier: item.id == null ? "" : item.id,
182                 "dactyl:command": item.command || "",
183                 href: item.item.url, highlight: "URL" },
184             text || ""]
185     ],
186
187     filter: function (str) ["span", { highlight: "Filter" }, str],
188
189     completionRow: function completionRow(item, highlightGroup) {
190         if (typeof icon == "function")
191             icon = icon();
192
193         if (highlightGroup) {
194             var text = item[0] || "";
195             var desc = item[1] || "";
196         }
197         else {
198             var text = this.processor[0].call(this, item, item.result);
199             var desc = this.processor[1].call(this, item, item.description);
200         }
201
202         return ["div", { highlight: highlightGroup || "CompItem", style: "white-space: nowrap" },
203                    /* The non-breaking spaces prevent empty elements
204                     * from pushing the baseline down and enlarging
205                     * the row.
206                     */
207                    ["li", { highlight: "CompResult " + item.highlight },
208                        text, "\u00a0"],
209                    ["li", { highlight: "CompDesc" },
210                        desc, "\u00a0"]];
211     },
212
213     helpLink: function (token, text, type) {
214         if (!help.initialized)
215             util.dactyl.initHelp();
216
217         let topic = token; // FIXME: Evil duplication!
218         if (/^\[.*\]$/.test(topic))
219             topic = topic.slice(1, -1);
220         else if (/^n_/.test(topic))
221             topic = topic.slice(2);
222
223         if (help.initialized && !Set.has(help.tags, topic))
224             return ["span", { highlight: type || ""}, text || token];
225
226         type = type || (/^'.*'$/.test(token)   ? "HelpOpt" :
227                         /^\[.*\]$|^E\d{3}$/.test(token) ? "HelpTopic" :
228                         /^:\w/.test(token)     ? "HelpEx"  : "HelpKey");
229
230         return ["a", { highlight: "InlineHelpLink " + type, tag: topic,
231                        href: "dactyl://help-tag/" + topic,
232                        "dactyl:command": "dactyl.help" },
233                     text || topic];
234     },
235     HelpLink: function (token) {
236         if (!help.initialized)
237             util.dactyl.initHelp();
238
239         let topic = token; // FIXME: Evil duplication!
240         if (/^\[.*\]$/.test(topic))
241             topic = topic.slice(1, -1);
242         else if (/^n_/.test(topic))
243             topic = topic.slice(2);
244
245         if (help.initialized && !Set.has(help.tags, topic))
246             return token;
247
248         let tag = (/^'.*'$/.test(token)            ? "o" :
249                    /^\[.*\]$|^E\d{3}$/.test(token) ? "t" :
250                    /^:\w/.test(token)              ? "ex"  : "k");
251
252         topic = topic.replace(/^'(.*)'$/, "$1");
253         return [tag, { xmlns: "dactyl" }, topic];
254     },
255     linkifyHelp: function linkifyHelp(str, help) {
256         let re = util.regexp(literal(/*
257             (?P<pre> [/\s]|^)
258             (?P<tag> '[\w-]+' | :(?:[\w-]+!?|!) | (?:._)?<[\w-]+>\w* | \b[a-zA-Z]_(?:[\w[\]]+|.) | \[[\w-;]+\] | E\d{3} )
259             (?=      [[\)!,:;./\s]|$)
260         */), "gx");
261         return this.highlightSubstrings(str, (function () {
262             for (let res in re.iterate(str))
263                 yield [res.index + res.pre.length, res.tag.length];
264         })(), this[help ? "HelpLink" : "helpLink"]);
265     },
266
267
268     // Fixes some strange stack rewinds on NS_ERROR_OUT_OF_MEMORY
269     // exceptions that we can't catch.
270     stringify: function stringify(arg) {
271         if (!callable(arg))
272             return String(arg);
273
274         try {
275             this._sandbox.arg = arg;
276             return Cu.evalInSandbox("String(arg)", this._sandbox);
277         }
278         finally {
279             this._sandbox.arg = null;
280         }
281     },
282
283     _sandbox: Class.Memoize(function () Cu.Sandbox(Cu.getGlobalForObject(global),
284                                                    { wantXrays: false })),
285
286     // if "processStrings" is true, any passed strings will be surrounded by " and
287     // any line breaks are displayed as \n
288     highlight: function highlight(arg, processStrings, clip, bw) {
289         // some objects like window.JSON or getBrowsers()._browsers need the try/catch
290         try {
291             let str = this.stringify(arg);
292             if (clip)
293                 str = util.clip(str, clip);
294             switch (arg == null ? "undefined" : typeof arg) {
295             case "number":
296                 return ["span", { highlight: "Number" }, str];
297             case "string":
298                 if (processStrings)
299                     str = str.quote();
300                 return ["span", { highlight: "String" }, str];
301             case "boolean":
302                 return ["span", { highlight: "Boolean" }, str];
303             case "function":
304                 if (arg instanceof Ci.nsIDOMElement) // wtf?
305                     return util.objectToString(arg, !bw);
306
307                 str = str.replace("/* use strict */ \n", "/* use strict */ ");
308                 if (processStrings)
309                     return ["span", { highlight: "Function" },
310                                 str.replace(/\{(.|\n)*(?:)/g, "{ ... }")];
311                 arg = String(arg).replace("/* use strict */ \n", "/* use strict */ ");
312                 return arg;
313             case "undefined":
314                 return ["span", { highlight: "Null" }, "undefined"];
315             case "object":
316                 if (arg instanceof Ci.nsIDOMElement)
317                     return util.objectToString(arg, !bw);
318                 if (arg instanceof util.Magic)
319                     return String(arg);
320
321                 if (processStrings && false)
322                     str = template._highlightFilter(str, "\n",
323                                                     function () ["span", { highlight: "NonText" },
324                                                                      "^J"]);
325                 return ["span", { highlight: "Object" }, str];
326             case "xml":
327                 return arg;
328             default:
329                 return "<unknown type>";
330             }
331         }
332         catch (e) {
333             return "<error: " + e + ">";
334         }
335     },
336
337     highlightFilter: function highlightFilter(str, filter, highlight, isURI) {
338         if (isURI)
339             str = util.losslessDecodeURI(str);
340
341         return this.highlightSubstrings(str, (function () {
342             if (filter.length == 0)
343                 return;
344
345             let lcstr = String.toLowerCase(str);
346             let lcfilter = filter.toLowerCase();
347             let start = 0;
348             while ((start = lcstr.indexOf(lcfilter, start)) > -1) {
349                 yield [start, filter.length];
350                 start += filter.length;
351             }
352         })(), highlight || template.filter);
353     },
354
355     highlightRegexp: function highlightRegexp(str, re, highlight) {
356         return this.highlightSubstrings(str, (function () {
357             for (let res in util.regexp.iterate(re, str))
358                 yield [res.index, res[0].length, res.wholeMatch ? [res] : res];
359         })(), highlight || template.filter);
360     },
361
362     highlightSubstrings: function highlightSubstrings(str, iter, highlight) {
363         if (!isString(str))
364             return str;
365
366         if (str == "")
367             return DOM.DOMString(str);
368
369         let s = [""];
370         let start = 0;
371         let n = 0, _i;
372         for (let [i, length, args] in iter) {
373             if (i == _i || i < _i)
374                 break;
375             _i = i;
376
377             s.push(str.substring(start, i),
378                    highlight.apply(this, Array.concat(args || str.substr(i, length))));
379             start = i + length;
380         }
381         s.push(str.substr(start));
382         return s;
383     },
384
385     highlightURL: function highlightURL(str, force) {
386         if (force || /^[a-zA-Z]+:\/\//.test(str))
387             return ["a", { highlight: "URL", href: str },
388                         util.losslessDecodeURI(str)];
389         else
390             return str;
391     },
392
393     icon: function (item, text) [
394         ["span", { highlight: "CompIcon" },
395             item.icon ? ["img", { src: item.icon }] : []],
396         ["span", { class: "td-strut" }],
397         text
398     ],
399
400     jumps: function jumps(index, elems) {
401         return ["table", {},
402                 ["tr", { style: "text-align: left;", highlight: "Title" },
403                     ["th", { colspan: "2" }, _("title.Jump")],
404                     ["th", {}, _("title.HPos")],
405                     ["th", {}, _("title.VPos")],
406                     ["th", {}, _("title.Title")],
407                     ["th", {}, _("title.URI")]],
408                 this.map(Iterator(elems), function ([idx, val])
409                     ["tr", {},
410                         ["td", { class: "indicator" }, idx == index ? ">" : ""],
411                         ["td", {}, Math.abs(idx - index)],
412                         ["td", {}, val.offset ? val.offset.x : ""],
413                         ["td", {}, val.offset ? val.offset.y : ""],
414                         ["td", { style: "width: 250px; max-width: 500px; overflow: hidden;" }, val.title],
415                         ["td", {},
416                             ["a", { href: val.URI.spec, highlight: "URL jump-list" },
417                                 util.losslessDecodeURI(val.URI.spec)]]])];
418     },
419
420
421     options: function options(title, opts, verbose) {
422         return ["table", {},
423                 ["tr", { highlight: "Title", align: "left" },
424                     ["th", {}, "--- " + title + " ---"]],
425                 this.map(opts, function (opt)
426                     ["tr", {},
427                         ["td", {},
428                             ["div", { highlight: "Message" },
429                                 ["span", { style: opt.isDefault ? "" : "font-weight: bold" },
430                                     opt.pre, opt.name],
431                                 ["span", {}, opt.value],
432                                 opt.isDefault || opt.default == null ? "" : ["span", { class: "extra-info" }, " (default: ", opt.default, ")"]],
433                             verbose && opt.setFrom ? ["div", { highlight: "Message" },
434                                                          "       Last set from ",
435                                                          template.sourceLink(opt.setFrom)] : ""]])];
436     },
437
438     sourceLink: function (frame) {
439         let url = util.fixURI(frame.filename || "unknown");
440         let path = util.urlPath(url);
441
442         return ["a", { "dactyl:command": "buffer.viewSource",
443                         href: url, path: path, line: frame.lineNumber,
444                         highlight: "URL" },
445             path + ":" + frame.lineNumber];
446     },
447
448     table: function table(title, data, indent) {
449         let table = ["table", {},
450             ["tr", { highlight: "Title", align: "left" },
451                 ["th", { colspan: "2" }, title]],
452             this.map(data, function (datum)
453                 ["tr", {},
454                     ["td", { style: "font-weight: bold; min-width: 150px; padding-left: " + (indent || "2ex") }, datum[0]],
455                     ["td", {}, datum[1]]])];
456
457         if (table[3].length)
458             return table;
459     },
460
461     tabular: function tabular(headings, style, iter) {
462         let self = this;
463         // TODO: This might be mind-bogglingly slow. We'll see.
464         return ["table", {},
465             ["tr", { highlight: "Title", align: "left" },
466                 this.map(headings, function (h)
467                     ["th", {}, h])],
468             this.map(iter, function (row)
469                 ["tr", {},
470                     self.map(Iterator(row), function ([i, d])
471                         ["td", { style: style[i] || "" }, d])])];
472     },
473
474     usage: function usage(iter, format) {
475         let self = this;
476
477         format = format || {};
478         let desc = format.description || function (item) self.linkifyHelp(item.description);
479         let help = format.help || function (item) item.name;
480         function sourceLink(frame) {
481             let source = self.sourceLink(frame);
482             source[1]["dactyl:hint"] = source[2];
483             return source;
484         }
485         return ["table", {},
486             format.headings ?
487                 ["thead", { highlight: "UsageHead" },
488                     ["tr", { highlight: "Title", align: "left" },
489                         this.map(format.headings, function (h) ["th", {}, h])]] :
490                 [],
491             format.columns ?
492                 ["colgroup", {},
493                     this.map(format.columns, function (c) ["col", { style: c }])] :
494                 [],
495             ["tbody", { highlight: "UsageBody" },
496                 this.map(iter, function (item)
497                     // Urgh.
498                     let (name = item.name || item.names[0], frame = item.definedAt)
499                         ["tr", { highlight: "UsageItem" },
500                             ["td", { style: "padding-right: 2em;" },
501                                 ["span", { highlight: "Usage Link" },
502                                     !frame ? name :
503                                         [self.helpLink(help(item), name, "Title"),
504                                          ["span", { highlight: "LinkInfo" },
505                                             _("io.definedAt"), " ",
506                                             sourceLink(frame)]]]],
507                             item.columns ? self.map(item.columns, function (c) ["td", {}, c]) : [],
508                             ["td", {}, desc(item)]])]]
509     }
510 });
511
512 endModule();
513
514 // vim: set fdm=marker sw=4 ts=4 et ft=javascript: