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