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