e62bbee67fa331ce7df3d4f68349c8f2d540b389
[roobuilder] / src / JsRender / Roo.vala
1 /*
2  * Renderer for Javascript output (roo library based)
3  * 
4  * - translation support
5  * -  doubleStringProps contains elements that are 'translable'
6  *    ** in the old method this our compression tool could extract them
7  *  - the  new idea is to make a list at the top of the javascript file
8  *    and output a map...
9  *    
10  * 
11  * 
12  * 
13  * 
14  */
15 namespace JsRender {
16
17     static int rid = 0; 
18    
19     class Roo : JsRender 
20     {
21          
22         bool disabled;
23
24  
25         
26         public Roo(Project.Roo project, string path) 
27         {
28             aconstruct( project, path);
29  
30             this.xtype = "Roo";
31              this.language = "js";
32             
33            this.content_type = "";
34            
35             //this.items = false;
36             //if (cfg.json) {
37             //    var jstr =  JSON.parse(cfg.json);
38             //    this.items = [ jstr ];
39             //    //console.log(cfg.items.length);
40             //    delete cfg.json; // not needed!
41             // }
42             this.modOrder = "001"; /// sequence id that this uses.
43             this.region = "center";
44             this.disabled = false;
45             
46             // super?!?!
47             this.id = "file-roo-%d".printf(rid++);
48             //console.dump(this);
49             // various loader methods..
50
51             string[]  dsp = { 
52                                 "title",
53                 "legend",
54                 "loadingText",
55                 "emptyText",
56                 "qtip",
57                 "value",
58                 "text",
59                 "emptyMsg",
60                 "displayMsg",
61                 "html",
62                 "headline",
63                 "header",
64                 "placeholder",
65                 "fieldLabel",
66                 "emptyTitle",
67                 "dialogTitle",
68                 "modalTitle",
69                 "boxLabel"
70                 
71                 };
72             for (var i=0;i<dsp.length;i++) {
73                 this.doubleStringProps.add(dsp[i]);
74             }
75
76             
77         }
78     
79  
80                 
81                 public   override void   removeFiles() {
82                         var html = GLib.Path.get_dirname(this.path) +"/templates/" + name + ".html";
83                         if (FileUtils.test(html, FileTest.EXISTS)) {
84                                 GLib.FileUtils.remove(html);
85                         }
86                         var js = GLib.Path.get_dirname(this.path) +"/" + name + ".html";
87                         if (FileUtils.test(js, FileTest.EXISTS)) {
88                                 GLib.FileUtils.remove(js);
89                         }
90                 }
91                 
92         public  override void  loadItems() throws GLib.Error // : function(cb, sync) == original was async.
93         {
94             
95                                  
96                         GLib.debug("load Items!");
97                         if (this.tree != null) {
98                                 return;
99                         }
100                         GLib.debug("load " + this.path);
101
102                         var pa = new Json.Parser();
103                         pa.load_from_file(this.path);
104                         var node = pa.get_root();
105
106                         if (node.get_node_type () != Json.NodeType.OBJECT) {
107                                 throw new Error.INVALID_FORMAT ("Unexpected element type %s", node.type_name ());
108                         }
109                         var obj = node.get_object ();
110                 
111                 
112                         this.modOrder = this.jsonHasOrEmpty(obj, "modOrder");
113                         this.name = this.jsonHasOrEmpty(obj, "name");
114                         this.parent = this.jsonHasOrEmpty(obj, "parent");
115                         this.permname = this.jsonHasOrEmpty(obj, "permname");
116                         this.title = this.jsonHasOrEmpty(obj, "title");
117                         this.modOrder = this.jsonHasOrEmpty(obj, "modOrder");
118                         if (obj.has_member("gen_extended")) { // should check type really..
119                                 this.gen_extended = obj.get_boolean_member("gen_extended");
120                         }
121                         var bjs_version_str = this.jsonHasOrEmpty(obj, "bjs-version");
122                         bjs_version_str = bjs_version_str == "" ? "1" : bjs_version_str;
123
124                         
125                         // load items[0] ??? into tree...
126                         if (obj.has_member("items") 
127                                 && 
128                                 obj.get_member("items").get_node_type() == Json.NodeType.ARRAY
129                                 &&
130                                 obj.get_array_member("items").get_length() > 0
131                         ) {
132                                 this.tree = new Node(); 
133                                 var ar = obj.get_array_member("items");
134                                 var tree_base = ar.get_object_element(0);
135                                 this.tree.loadFromJson(tree_base, int.parse(bjs_version_str));
136                         }
137                         this.loaded = true;
138                         this.toSource(); // force it to number the lines...
139
140             
141         }
142         
143                 public override string targetName()
144                 {
145                         string js;
146                         try {
147                                 Regex regex = new Regex("\\.(bjs|js)$");
148
149                                 js = regex.replace(this.path,this.path.length , 0 , ".js");
150                                 return js;
151                         } catch (RegexError e) {
152                                 this.name = "???";
153                                 print("count not make filename from path");
154                                 return this.name + ".js";
155                         }
156                         
157                 }
158                 public  override  void save()
159                 {
160                         
161                         GLib.debug("--- JsRender.Roo.save");
162                         GLib.debug("save() - reset transStrings\n");
163                         this.transStrings = new Gee.HashMap<string,string>();
164                         this.namedStrings = new Gee.HashMap<string,string>();
165                         this.findTransStrings(this.tree);
166                         
167                         this.saveBJS();
168
169                         // no tree..
170                         if (this.tree == null) {
171                                 return;
172                         }
173                         // now write the js file..
174                         var  js = this.targetName();
175                          
176
177
178                         //var d = new Date();
179                         var js_src = this.toSource();            
180                         //print("TO SOURCE in " + ((new Date()) - d) + "ms");
181                         try {
182                                 this.writeFile(js, js_src);            
183                         } catch (GLib.Error e ) {
184                                 print("Save failed\n");
185                         }
186                         // for bootstrap - we can write the HTML to the templates directory..
187                                  
188                         //var top = this.guessName(this.items[0]);
189                         //print ("TOP = " + top)
190                          
191                         
192                         
193                         
194                 }
195
196          public Project.Roo roo_project {
197                 set {}  
198                 get { 
199                         return (Project.Roo) this.project;
200                 }
201         }
202                         
203
204          
205         public override void saveHTML ( string html )
206         {
207                 GLib.debug ("SAVEHTML %s\n",  this.roo_project.runhtml);                 
208                 if (this.roo_project.runhtml == "") {
209                         return;
210                 }
211                  
212                 var top = this.tree.fqn();
213                 GLib.debug ("TOP = " + top + "\n" );
214                 if (top.index_of("Roo.bootstrap.") < 0 &&
215                                  top.index_of("Roo.mailer.") < 0
216                 ) {
217                 return;
218                 }
219                 
220                 
221                 //now write the js file..
222                 string fn;
223                 try {
224                         Regex regex = new Regex("\\.(bjs|js)$");
225
226                         fn = regex.replace(this.path,this.path.length , 0 , ".html");
227                 } catch (RegexError e) {
228                         this.name = "???";
229                         print("count not make filename from path");
230                         return;
231                 }
232                 var bn = GLib.Path.get_basename(fn);
233                 var dn = GLib.Path.get_dirname(fn);
234
235                 var targetdir = dn + (
236                         top.index_of("Roo.mailer.") < 0 ? "/templates" : "" );
237                               
238                 
239                 if (!FileUtils.test(targetdir, FileTest.IS_DIR)) {
240                         print("Skip save - templates folder does not exist : %s\n", targetdir);
241                         return;
242                 }
243            print("SAVE HTML (%d) -- %s\n",html.length, targetdir + "/" +  bn);
244                 try {
245                         this.writeFile(targetdir + "/" +  bn , html);            
246                 } catch (GLib.Error e ) {
247                         print("SaveHtml failed\n");
248                 }
249             
250             
251             
252         }
253
254                 public Gee.ArrayList<string> findxincludes(Node node,   Gee.ArrayList<string> ret)
255                 {
256                         
257                         if (node.props.has_key("* xinclude")) {
258                                 ret.add(node.props.get("* xinclude").val);
259                         }
260                         var items = node.readItems();
261                         for (var i =0; i < items.size; i++) {
262                                 this.findxincludes(items.get(i), ret);
263                         }
264                         return ret;
265                                 
266                 }
267                  
268
269                  
270                 public override void  findTransStrings(Node? node )
271                 {
272                         // iterate properties...
273                         // use doubleStringProps
274                         
275                         // flagging a translatable string..
276                         // the code would use string _astring to indicate a translatable string
277                         // the to use it it would do String.format(this._message, somedata);
278                         
279                         // loop through and find string starting with '_' 
280                         if (node == null) {
281                                 return;
282                         }               
283                         
284                         var named = new Gee.HashMap<string,string>();
285                         var name_prefix = "";
286                         
287                         var iter = node.props.map_iterator();
288                         while (iter.next()) {
289                                 // key formats : XXXX
290                                 // XXX - plain
291                                 // string XXX - with type
292                                 // $ XXX - with flag (no type)
293                                 // $ string XXX - with flag
294                                 
295                                 
296                                 var prop = iter.get_value();
297                                 var kname = prop.name;
298                                 var ktype = prop.rtype;
299
300
301                                 if (prop.ptype == NodePropType.RAW) {
302                                         continue;
303                                 }
304                                 // skip cms-id nodes...
305                                 if (kname == "html" && node.has("cms-id")) { 
306                                         continue;
307                                 }
308                                 var str = prop.val;
309                                 if (kname == "name") {
310                                         name_prefix = str;
311                                 }
312                                 
313                                 var chksum = GLib.Checksum.compute_for_string (ChecksumType.MD5, str.strip());
314                                 
315                                 if (this.doubleStringProps.index_of(kname) > -1) {
316                                         //GLib.debug("flag=%s type=%s name=%s : %s\n", prop.ptype.to_string(),ktype,kname,str);
317                                         this.transStrings.set(str,  chksum);
318                                         named.set("_" + kname, chksum);
319                                         continue;
320                                 }
321                                 
322                                 if (ktype.down() == "string" && kname[0] == '_') {
323                                         GLib.debug("flag=%s type=%s name=%s : %s\n", prop.ptype.to_string(),ktype,kname,str);
324                                         this.transStrings.set(str,   chksum);
325                                         named.set(kname, chksum);
326                                         continue;
327                                 }
328                                 
329                         }
330                         if (name_prefix != "") {
331                                 var niter = named.map_iterator();
332                                 while (niter.next()) {
333                                         this.namedStrings.set(name_prefix + niter.get_key(),niter.get_value());
334                                 }
335                          }
336
337                         var items = node.readItems();
338                         // iterate children..
339                         for (var i =0; i < items.size; i++) {
340                                 this.findTransStrings(items.get(i) );
341                         }
342                 
343                                 
344                 }  
345                 
346                 public string  transStringsToJs()
347                 {
348                         
349                         GLib.debug("Roo.transStringsToJs()\n");
350                         if (this.transStrings.size < 1) {
351                                 GLib.debug("Roo.transStringsToJs() size < 1?\n");
352                                 return "";
353                         }
354                          
355                         string[] kvs = {};
356                         
357                         var hash = new Gee.HashMap<string,string>();
358                         var iter = this.transStrings.map_iterator();
359                         while (iter.next()) {
360                                 hash.set(iter.get_value(), iter.get_key());
361                                 kvs +=  ("  '" + iter.get_value() + "' :" + 
362                                         this.tree.quoteString(iter.get_key())
363                                         );
364                         }
365                         
366                         var ret = " _strings : {\n" + string.joinv(",\n", kvs) + "\n },";
367
368                         
369                         string[] ns = {};
370                         var niter = this.namedStrings.map_iterator();
371                         while (niter.next()) {
372                                 var otext = hash.get(niter.get_value());
373                                 var com = " /* " + (otext.replace("*/", "* - /") + " */ ");
374
375                         
376                                 ns +=  ("  '" + niter.get_key() + "' : '" + niter.get_value() + "'" + com); 
377                         }
378                         if (ns.length > 0 ) {
379                                 ret += "\n _named_strings : {\n" + string.joinv(",\n", ns) + "\n },";
380                         }
381                         return ret;
382                 }       
383                 
384              
385         /**
386          * javascript used in Webkit preview 
387          */
388         
389         public override string  toSourcePreview()
390         {
391                         print("toSourcePreview() - reset transStrings\n");
392                         this.transStrings = new Gee.HashMap<string,string>();
393                         this.namedStrings = new Gee.HashMap<string,string>();
394                 
395                         print("to source preview\n");
396                         if (this.tree == null) {
397                                 return "";
398                         }
399                         this.findTransStrings(this.tree);
400                         var top = this.tree.fqn();
401                         var xinc = new Gee.ArrayList<string>(); 
402
403                         this.findxincludes(this.tree, xinc);
404                         print("got %d xincludes\n", xinc.size);
405                         var prefix_data = "";
406                         if (xinc.size > 0 ) {
407                                 for(var i = 0; i < xinc.size; i++) {
408                                         print("check xinclude:  %s\n", xinc.get(i));
409                                         var sf = this.project.getByName(xinc.get(i));
410                                         if (sf == null) {
411                                                 print("Failed to find file by name?\n");
412                                                 continue;
413                                         }
414                                         try {
415                                                 sf.loadItems();
416                                                 sf.findTransStrings(sf.tree);
417                                                 var xinc_str = sf.toSource();
418                                                 
419                                                 //string xinc_str;
420                                                 //FileUtils.get_contents(js, out xinc_str);
421                                                 prefix_data += "\n" + xinc_str + "\n";
422                                         } catch (GLib.Error e) {}
423                                         
424                                 }
425
426                         }
427
428                         
429                         
430                         //print(JSON.stringify(this.items, null,4));
431                                    
432                         if (top == null) {
433                                 print ("guessname returned false");
434                                 return "";
435                         }
436
437
438                         if (top.contains("Dialog")) {
439                                 return prefix_data + this.toSourceDialog(true);
440                         }
441
442                         if (top.contains("Modal")) {
443                                 return prefix_data + this.toSourceModal(true);
444                         }
445                         if (top.contains("Popover")) {
446                                 return prefix_data + this.toSourceModal(true);
447                         }
448                         return prefix_data + this.toSourceLayout(true);
449                                 
450                                 
451             
452         }
453         public override void setSource(string str) {}
454         /**
455          * This needs to use some options on the project
456          * to determine how the file is output..
457          * 
458          * At present we are hard coding it..
459          * 
460          * 
461          */
462         public override string toSourceCode() 
463         {
464                         this.transStrings = new Gee.HashMap<string,string>();
465                         this.namedStrings = new Gee.HashMap<string,string>();
466                         this.findTransStrings(this.tree);
467                         return this.toSource();
468                 }
469          
470         public override string toSource()
471         {
472             // dump the file tree back out to a string.
473             
474             // we have 2 types = dialogs and components
475             // 
476             
477             
478                         if (this.tree == null) {
479                                 return "";
480                         }
481             var top = this.tree.fqn();
482             if (top == null) {
483                 return "";
484             }
485             
486             
487             
488             // get the translatable strings.. = we reload them again so calling methods get the right data...
489             this.transStrings = new Gee.HashMap<string,string>();
490             this.namedStrings = new Gee.HashMap<string,string>();
491                         this.findTransStrings(this.tree);
492             
493             
494             if (top.contains("Dialog")) {
495                 return this.toSourceDialog(false);
496             }
497             
498             if (top.contains("Modal")) {
499                 return this.toSourceModal(false);
500             }
501             
502             if (top.contains("Popover")) {
503                 return this.toSourceModal(false);
504             }
505             
506             return this.toSourceLayout(false);
507             
508             /*
509             eventually support 'classes??'
510              return this.toSourceStdClass();
511             */
512               
513         }
514         
515         /**
516                  * 
517                  * munge JSON tree into Javascript code.
518                  *
519                  * NOTE - needs a deep copy of original tree, before starting..
520                  *     - so that it does not modify current..
521                  * 
522                  * FIXME: + or / prefixes to properties hide it from renderer.
523                  * FIXME: '*props' - not supported by this.. ?? - upto rendering code..
524                  * FIXME: needs to understand what properties might be translatable (eg. double quotes)
525                  * 
526                  * @arg {object} obj the object or array to munge..
527                  * @arg {boolean} isListener - is the array being sent a listener..
528                  * @arg {string} pad - the padding to indent with. 
529                  */
530                 
531                 public string mungeToStringWrap(string pad, string prefix, string suffix)
532                 {
533                         if (this.tree == null) {
534                                 return "";
535                         }
536                         var x = new NodeToJs(this.tree, this.doubleStringProps, pad, null);
537                         x.renderer = this;
538                         x.cur_line = prefix.split("\n").length;
539                         
540                         var ret = x.munge();
541                         //var nret = x.ret;
542                         
543                         // output both files.. so we can diff them...
544                         //this.writeFile("/tmp/old.js", ret);
545                         //this.writeFile("/tmp/new.js", nret);                  
546                         return prefix +  ret + suffix;
547                         
548                     
549                 }
550         
551         
552        
553         public string outputHeader()
554         {
555                 string[] s = {
556                         "//<script type=\"text/javascript\">",
557                         "",
558                         "// Auto generated file - created by app.Builder.js- do not edit directly (at present!)",
559                         ""
560                    
561                 };  
562                 var ret=  string.joinv("\n",s);
563                 var bits = this.name.split(".");
564                 if (bits.length > 1) {
565                         ret += "\nRoo.namespace(\'" + 
566                                 this.name.substring(0, this.name.length - (bits[bits.length-1].length + 1)) +
567                                 "');\n";
568                                 
569                 }
570                 /// genericlly used..
571                   
572                 return ret;
573             
574        
575         }
576         // a standard dialog module.
577         // fixme - this could be alot neater..
578         public string toSourceDialog(bool isPreview) 
579         {
580             
581             //var items = JSON.parse(JSON.stringify(this.items[0]));
582             
583     
584            
585  
586             string[] adda = { " = {",
587                 "",
588                 this.transStringsToJs() ,
589                 "",
590                 " dialog : false,",
591                 " callback:  false,",
592                 "",   
593                 " show : function(data, cb)",
594                 " {",
595                 "  if (!this.dialog) {",
596                 "   this.create();",
597                 "  }",
598                 "",
599                 "  this.callback = cb;",
600                 "  this.data = data;",
601                 "  this.dialog.show.apply(this.dialog,  Array.prototype.slice.call(arguments).slice(2));",
602                 "  if (this.form) {",
603                 "   this.form.reset();",
604                 "   this.form.setValues(data);",
605                 "   this.form.fireEvent('actioncomplete', this.form,  { type: 'setdata', data: data });",
606                 "  }",
607                 "",   
608                 " },",
609                 "",
610                 " create : function()",
611                 " {",
612                 "   var _this = this;",
613                 "   this.dialog = Roo.factory(" 
614             };
615             string[] addb = {  
616                    ");",
617                 " }",
618                 "};",
619                 ""
620             };
621              
622             return this.mungeToStringWrap("    ",   
623                         this.outputHeader() + "\n" + this.name + string.joinv("\n", adda), //header
624                         string.joinv("\n", addb) // footer
625                 );
626              
627              
628              
629              
630         }
631         /**
632          Bootstrap modal dialog 
633          
634         */
635         
636         
637         public string toSourceModal(bool isPreview) 
638         {
639             
640             
641             //var items = JSON.parse(JSON.stringify(this.items[0]));
642                      /*
643             
644             old caller:
645             
646             xxxxxx.show({});
647             
648             a = new xxxxxx();
649             a.show();
650             
651             XXXX = function() {};
652             Roo.apply(XXXX.prototype, { .... });
653             Roo.apply(XXXX, XXX.prototype);            
654             
655             */
656             
657             string[] adda = { "{",
658                 "",
659                 this.transStringsToJs() ,
660                 "",
661                 " dialog : false,",
662                 " callback:  false,",
663                 "",   
664                 " show : function(data, cb)",
665                 " {",
666                 "  if (!this.dialog) {",
667                 "   this.create();",
668                 "  }",
669                 "",
670                 "  this.callback = cb;",
671                 "  this.data = data;",
672                 "  this.dialog.show.apply(this.dialog,  Array.prototype.slice.call(arguments).slice(2));",
673                 "  if (this.form) {",
674                 "   this.form.reset();",
675                 "   this.form.setValues(data);",
676                 "   this.form.fireEvent('actioncomplete', this.form,  { type: 'setdata', data: data });",
677                 "  }",
678                 "",   
679                 " },",
680                 "",
681                 " create : function()",
682                 " {",
683                 "  var _this = this;",
684                 "  this.dialog = Roo.factory("
685             };
686             string[] addb =  {
687                 "  );",
688                 " }",
689                 "}"
690             };
691                         return this.mungeToStringWrap("    ",   
692                                 this.outputHeader() + "\n" +  
693                         this.name + "= function() {}\n" + 
694                         "Roo.apply("+this.name + ".prototype, " + string.joinv("\n", adda), // header
695                         // body goes here from the function..
696                         string.joinv("\n", addb) + ");\n" + // footer
697                         "Roo.apply("+this.name +", " + this.name + ".prototype);\n"
698                 );
699              
700              
701              
702         }
703          
704         
705         
706         
707         
708         public string   pathToPart()
709         {
710             var dir = Path.get_basename(Path.get_dirname(this.path));
711             var ar = dir.split(".");
712             var modname = ar[ar.length-1];
713             
714             // now we have the 'module name'..
715             var fbits = Path.get_basename(this.path).split(".");
716             
717              
718             var npart = fbits[fbits.length - 2]; // this should be 'AdminProjectManager' for example...
719             if (modname.length < npart.length && npart.substring(0, modname.length) == modname) {
720                 npart = npart.substring(modname.length);
721             }
722             return "[" + this.tree.quoteString(modname) + ", " + this.tree.quoteString(npart) + " ]";
723             //return ret;
724             
725             
726             
727             
728         }
729         
730         // a layout compoent 
731         public string toSourceLayout(bool isPreview) 
732         {
733           
734             
735                 if (isPreview) {
736                         //       topItem.region = 'center';
737                         //    topItem.background = false;
738                 }
739             
740                 var  modkey = this.modOrder + "-" +   this.name;
741                 try {
742                         var reg = new Regex("[^A-Za-z.]+");
743             
744                          modkey = this.modOrder + "-" + reg.replace(this.name, this.name.length, 0 , "-");
745             } catch (RegexError e) {
746                         //noop..
747             }
748                 string  parent =   (this.parent.length > 0 ?  "'" + this.parent + "'" :  "false");
749
750                 
751                 
752                 if (isPreview) {
753                         // set to false to ensure this is the top level..
754                         parent = "false";
755                                 var topnode = this.tree.fqn();
756                                 print("topnode = %s\n", topnode);
757                         if (GLib.Regex.match_simple("^Roo\\.bootstrap\\.",topnode) &&
758                             topnode != "Roo.bootstrap.Body"
759                         ) {
760                                 parent = "\"#bootstrap-body\"";
761                         }
762                           
763                 }
764             
765             
766             
767             var pref = this.outputHeader() + "\n" +
768                         
769                         this.name  +  " = new Roo.XComponent({\n" +
770                         "\n" + 
771                         this.transStringsToJs()  +    "\n" +
772                 "\n" +
773                         "  part     :  "+ this.pathToPart() + ",\n" +
774                                 /// critical used by builder to associate modules/parts/persm
775                         "  order    : '" +modkey+"',\n" +
776                         "  region   : '" + this.region   +"',\n" +
777                         "  parent   : "+ parent + ",\n" +
778                         "  name     : " + this.tree.quoteString(this.title.length > 0 ? this.title : "unnamed module") + ",\n" +
779                         "  disabled : " + (this.disabled ? "true" : "false") +", \n" +
780                         "  permname : '" + (this.permname.length > 0 ? this.permname : "") +"', \n" +
781                             
782                        // "    tree : function() { return this._tree(); },\n" +   //BC
783                         "  _tree : function(_data)\n" +
784                         "  {\n" +
785                         "   var _this = this;\n" + // bc
786                         "   var MODULE = this;\n" + /// this looks like a better name.
787                         "   return ";
788                         
789                     return this.mungeToStringWrap("   ", pref,  ";" +
790                         "  }\n" +
791                         "});\n"
792                 );
793                       
794               
795         }
796             
797         
798                  public override string toGlade() 
799                 {
800                         return "Roo files do not convert to glade";
801                 }
802      
803     }
804 }