1 /* See license.txt for terms of usage */
  2 
  3 // xxxFlorent: needs vigilant tests
  4 
  5 define([
  6     "firebug/lib/string",
  7 ],
  8 function(Str) {
  9 
 10 // ********************************************************************************************* //
 11 
 12 var Domplate = {};
 13 
 14 // xxxFlorent: not so pretty... maybe create a log function,
 15 //             so either FBTrace.sysout or console.log are called (when a debug variable is set)
 16 window.FBTrace = window.FBTrace || {};
 17 
 18 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
 19 
 20 function DomplateTag(tagName)
 21 {
 22     this.tagName = tagName;
 23 }
 24 
 25 Domplate.DomplateTag = DomplateTag;
 26 
 27 function DomplateEmbed()
 28 {
 29 }
 30 
 31 function DomplateLoop()
 32 {
 33 }
 34 
 35 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
 36 
 37 var womb = null;
 38 var uid = 0;
 39 
 40 // xxxHonza: the only global should be Firebug object.
 41 var domplate = function()
 42 {
 43     var lastSubject;
 44     for (var i = 0; i < arguments.length; ++i)
 45         lastSubject = lastSubject ? copyObject(lastSubject, arguments[i]) : arguments[i];
 46 
 47     for (var name in lastSubject)
 48     {
 49         var val = lastSubject[name];
 50         if (isTag(val))
 51         {
 52             if (val.tag.subject)
 53             {
 54                 // Clone the entire domplate tag, e.g. DIV(), that is derived from
 55                 // an existing template. This allows to hold correct 'subject'
 56                 // reference that is used when executing callbacks implemented by
 57                 // templates. Note that 'subject' points to the current template object.
 58                 // See issue: http://code.google.com/p/fbug/issues/detail?id=4425
 59                 lastSubject[name] = val = copyObject({}, val);
 60                 val.tag = copyObject({}, val.tag);
 61             }
 62             val.tag.subject = lastSubject;
 63         }
 64     }
 65 
 66     return lastSubject;
 67 };
 68 
 69 domplate.context = function(context, fn)
 70 {
 71     var lastContext = domplate.lastContext;
 72     domplate.topContext = context;
 73     fn.apply(context);
 74     domplate.topContext = lastContext;
 75 };
 76 
 77 // xxxFlorent: should we keep that?
 78 Domplate.domplate = domplate;
 79 
 80 Domplate.TAG = function()
 81 {
 82     var embed = new DomplateEmbed();
 83     return embed.merge(arguments);
 84 };
 85 
 86 Domplate.FOR = function()
 87 {
 88     var loop = new DomplateLoop();
 89     return loop.merge(arguments);
 90 };
 91 
 92 DomplateTag.prototype =
 93 {
 94     /**
 95      * Initializer for DOM templates. Called to create new Functions objects like TR, TD,
 96      * OBJLINK, etc. See defineTag
 97      *
 98      * @param args keyword argments for the template, the {} brace stuff after the tag name,
 99      *      eg TR({...}, TD(...
100      * @param oldTag a nested tag, eg the TD tag in TR({...}, TD(...
101      */
102     merge: function(args, oldTag)
103     {
104         if (oldTag)
105             this.tagName = oldTag.tagName;
106 
107         this.context = oldTag ? oldTag.context : null;  // normally null on construction
108         this.subject = oldTag ? oldTag.subject : null;
109         this.attrs = oldTag ? copyObject(oldTag.attrs) : {};
110         this.classes = oldTag ? copyObject(oldTag.classes) : {};
111         this.props = oldTag ? copyObject(oldTag.props) : null;
112         this.listeners = oldTag ? copyArray(oldTag.listeners) : null;
113         this.children = oldTag ? copyArray(oldTag.children) : [];
114         this.vars = oldTag ? copyArray(oldTag.vars) : [];
115 
116         var attrs = args.length ? args[0] : null;
117         var hasAttrs = typeof(attrs) == "object" && !isTag(attrs);
118 
119         // Do not clear children, they can be copied from the oldTag.
120         //this.children = [];
121 
122         if (domplate.topContext)
123             this.context = domplate.topContext;
124 
125         if (args.length)
126             parseChildren(args, hasAttrs ? 1 : 0, this.vars, this.children);
127 
128         if (hasAttrs)
129             this.parseAttrs(attrs);
130 
131         return creator(this, DomplateTag);
132     },
133 
134     parseAttrs: function(args)
135     {
136         for (var name in args)
137         {
138             var val = parseValue(args[name]);
139             readPartNames(val, this.vars);
140 
141             if (name.lastIndexOf("on", 0) === 0)
142             {
143                 var eventName = name.substr(2);
144                 if (!this.listeners)
145                     this.listeners = [];
146                 this.listeners.push(eventName, val);
147             }
148             else if (name[0] == "_")
149             {
150                 var propName = name.substr(1);
151                 if (!this.props)
152                     this.props = {};
153                 this.props[propName] = val;
154             }
155             else if (name[0] == "$")
156             {
157                 var className = name.substr(1);
158                 if (!this.classes)
159                     this.classes = {};
160                 this.classes[className] = val;
161             }
162             else
163             {
164                 if (name == "class" && this.attrs.hasOwnProperty(name) )
165                     this.attrs[name] += " " + val;
166                 else
167                     this.attrs[name] = val;
168             }
169         }
170     },
171 
172     compile: function()
173     {
174         if (this.renderMarkup)
175             return;
176 
177         this.compileMarkup();
178         this.compileDOM();
179     },
180 
181     compileMarkup: function()
182     {
183         this.markupArgs = [];
184         var topBlock = [], topOuts = [], blocks = [], info = {args: this.markupArgs, argIndex: 0};
185 
186         this.generateMarkup(topBlock, topOuts, blocks, info);
187         this.addCode(topBlock, topOuts, blocks);
188 
189         var fnBlock = ['(function (__code__, __context__, __in__, __out__'];
190         for (var i = 0; i < info.argIndex; ++i)
191             fnBlock.push(', s', i);
192         fnBlock.push(') {\n');
193 
194         if (this.subject)
195             fnBlock.push('with (this) {\n');
196         if (this.context)
197             fnBlock.push('with (__context__) {\n');
198         fnBlock.push('with (__in__) {\n');
199 
200         fnBlock.push.apply(fnBlock, blocks);
201 
202         if (this.subject)
203             fnBlock.push('}\n');
204         if (this.context)
205             fnBlock.push('}\n');
206 
207         fnBlock.push('}})\n');
208 
209         function __link__(tag, code, outputs, args)
210         {
211             if (!tag || !tag.tag)
212             {
213                 if (FBTrace.DBG_DOMPLATE)
214                 {
215                     FBTrace.sysout("domplate.Empty tag object passed to __link__ " +
216                         "(compileMarkup). Ignoring element.");
217                 }
218                 return;
219             }
220 
221             tag.tag.compile();
222 
223             var tagOutputs = [];
224             var markupArgs = [code, tag.tag.context, args, tagOutputs];
225             markupArgs.push.apply(markupArgs, tag.tag.markupArgs);
226             tag.tag.renderMarkup.apply(tag.tag.subject, markupArgs);
227 
228             outputs.push(tag);
229             outputs.push(tagOutputs);
230         }
231 
232         function __escape__(value)
233         {
234             // xxxFlorent: What should do that function? no API or safe workaround to do this?
235             return Str.escapeForElementAttribute(value);
236         }
237 
238         function isArray(it)
239         {
240             return Object.prototype.toString.call(it) === "[object Array]";
241         }
242 
243         function __loop__(iter, outputs, fn)
244         {
245             var iterOuts = [];
246             outputs.push(iterOuts);
247 
248             if (!iter)
249                 return;
250 
251             if (isArray(iter) || iter instanceof NodeList)
252                 iter = new ArrayIterator(iter);
253 
254             var value;
255             try
256             {
257                 while (1)
258                 {
259                     value = iter.next();
260                     var itemOuts = [0,0];
261                     iterOuts.push(itemOuts);
262                     fn.apply(this, [value, itemOuts]);
263                 }
264             }
265             catch (exc)
266             {
267                 if (exc != StopIteration && FBTrace.DBG_ERRORS)
268                     FBTrace.sysout("domplate; __loop__ EXCEPTION " +
269                         (value ? value.name : "no value") + ", " + exc, exc);
270 
271                 // Don't throw the exception, many built in objects in Firefox throws exceptions
272                 // these days and it breaks the UI. We can remove as soon as:
273                 // 389002 and 455013 are fixed.
274                 //if (exc != StopIteration)
275                 //    throw exc;
276             }
277         }
278 
279         if (FBTrace.DBG_DOMPLATE)
280         {
281             fnBlock.push("//@ sourceURL=chrome://firebug/compileMarkup_" +
282                 (this.tagName?this.tagName:'')+"_"+(uid++)+".js\n");
283         }
284 
285         var js = fnBlock.join("");
286         this.renderMarkup = eval(js);
287     },
288 
289     getVarNames: function(args)
290     {
291         if (this.vars)
292             args.push.apply(args, this.vars);
293 
294         for (var i = 0; i < this.children.length; ++i)
295         {
296             var child = this.children[i];
297             if (isTag(child))
298                 child.tag.getVarNames(args);
299             else if (child instanceof Parts)
300             {
301                 for (var i = 0; i < child.parts.length; ++i)
302                 {
303                     if (child.parts[i] instanceof Variables)
304                     {
305                         var name = child.parts[i].names[0];
306                         var names = name.split(".");
307                         args.push(names[0]);
308                     }
309                 }
310             }
311         }
312     },
313 
314     generateMarkup: function(topBlock, topOuts, blocks, info)
315     {
316         if (FBTrace.DBG_DOMPLATE)
317             var beginBlock = topBlock.length;
318 
319         topBlock.push(',"<', this.tagName, '"');
320 
321         for (var name in this.attrs)
322         {
323             if (name != "class")
324             {
325                 var val = this.attrs[name];
326                 topBlock.push(', " ', name, '=\\""');
327                 addParts(val, ',', topBlock, info, true);
328                 topBlock.push(', "\\""');
329             }
330         }
331 
332         if (this.listeners)
333         {
334             for (var i = 0; i < this.listeners.length; i += 2)
335                 readPartNames(this.listeners[i+1], topOuts);
336         }
337 
338         if (this.props)
339         {
340             for (var name in this.props)
341                 readPartNames(this.props[name], topOuts);
342         }
343 
344         if ( this.attrs.hasOwnProperty("class") || this.classes)
345         {
346             topBlock.push(', " class=\\""');
347             if (this.attrs.hasOwnProperty("class"))
348                 addParts(this.attrs["class"], ',', topBlock, info, true);
349               topBlock.push(', " "');
350             for (var name in this.classes)
351             {
352                 topBlock.push(', (');
353                 addParts(this.classes[name], '', topBlock, info);
354                 topBlock.push(' ? "', name, '" + " " : "")');
355             }
356             topBlock.push(', "\\""');
357         }
358         topBlock.push(',">"');
359 
360         this.generateChildMarkup(topBlock, topOuts, blocks, info);
361 
362         // <br> element doesn't use end tag.
363         if (this.tagName != "br")
364             topBlock.push(',"</', this.tagName, '>"');
365 
366         if (FBTrace.DBG_DOMPLATE)
367             FBTrace.sysout("DomplateTag.generateMarkup " + this.tagName + ": " +
368                 topBlock.slice( - topBlock.length + beginBlock).join("").replace("\n"," "),
369                 {listeners: this.listeners, props: this.props, attrs: this.attrs});
370 
371     },
372 
373     generateChildMarkup: function(topBlock, topOuts, blocks, info)
374     {
375         for (var i = 0; i < this.children.length; ++i)
376         {
377             var child = this.children[i];
378             if (isTag(child))
379                 child.tag.generateMarkup(topBlock, topOuts, blocks, info);
380             else
381                 addParts(child, ',', topBlock, info, true);
382         }
383     },
384 
385     addCode: function(topBlock, topOuts, blocks)
386     {
387         if (topBlock.length)
388         {
389             blocks.push('__code__.push(""', topBlock.join(""), ');\n');
390             if (FBTrace.DBG_DOMPLATE)
391                 blocks.push('FBTrace.sysout("addCode "+__code__.join(""));\n');
392         }
393 
394         if (topOuts.length)
395             blocks.push('__out__.push(', topOuts.join(","), ');\n');
396         topBlock.splice(0, topBlock.length);
397         topOuts.splice(0, topOuts.length);
398     },
399 
400     addLocals: function(blocks)
401     {
402         var varNames = [];
403         this.getVarNames(varNames);
404 
405         var map = {};
406         for (var i = 0; i < varNames.length; ++i)
407         {
408             var name = varNames[i];
409             if ( map.hasOwnProperty(name) )
410                 continue;
411 
412             map[name] = 1;
413             var names = name.split(".");
414             blocks.push('var ', names[0] + ' = ' + '__in__.' + names[0] + ';\n');
415         }
416     },
417 
418     compileDOM: function()
419     {
420         var path = [];
421         var blocks = [];
422         this.domArgs = [];
423         path.embedIndex = 0;
424         path.loopIndex = 0;
425         path.staticIndex = 0;
426         path.renderIndex = 0;
427         var nodeCount = this.generateDOM(path, blocks, this.domArgs);
428 
429         var fnBlock = ['(function (root, context, o'];
430         for (var i = 0; i < path.staticIndex; ++i)
431             fnBlock.push(', ', 's'+i);
432         for (var i = 0; i < path.renderIndex; ++i)
433             fnBlock.push(', ', 'd'+i);
434 
435         fnBlock.push(') {\n');
436         for (var i = 0; i < path.loopIndex; ++i)
437             fnBlock.push('var l', i, ' = 0;\n');
438         for (var i = 0; i < path.embedIndex; ++i)
439             fnBlock.push('var e', i, ' = 0;\n');
440 
441         if (this.subject)
442             fnBlock.push('with (this) {\n');
443         if (this.context)
444             fnBlock.push('with (context) {\n');
445 
446         fnBlock.push(blocks.join(""));
447 
448         if (this.context)
449             fnBlock.push('}\n');
450         if (this.subject)
451             fnBlock.push('}\n');
452 
453         fnBlock.push('return ', nodeCount, ';\n');
454         fnBlock.push('})\n');
455 
456         function __bind__(object, fn)
457         {
458             return function(event) { return fn.apply(object, [event]); }
459         }
460 
461         function __link__(node, tag, args)
462         {
463             if (!tag || !tag.tag)
464             {
465                 if (FBTrace.DBG_DOMPLATE)
466                 {
467                     FBTrace.sysout("domplate.Empty tag object passed to __link__ " +
468                         "(compileDOM). Ignoring element.");
469                 }
470                 return;
471             }
472 
473             tag.tag.compile();
474 
475             var domArgs = [node, tag.tag.context, 0];
476             domArgs.push.apply(domArgs, tag.tag.domArgs);
477             domArgs.push.apply(domArgs, args);
478 
479             return tag.tag.renderDOM.apply(tag.tag.subject, domArgs);
480         }
481 
482         var self = this;
483         function __loop__(iter, fn)
484         {
485             var nodeCount = 0;
486             for (var i = 0; i < iter.length; ++i)
487             {
488                 iter[i][0] = i;
489                 iter[i][1] = nodeCount;
490                 nodeCount += fn.apply(this, iter[i]);
491             }
492             return nodeCount;
493         }
494 
495         // start at a given node |parent|, then index recursively into its children using
496         // arguments 2, 3, ... The primary purpose of the 'path' is to name variables in the
497         // generated code
498         function __path__(parent, offset)
499         {
500             var root = parent;
501 
502             for (var i = 2; i < arguments.length; ++i)
503             {
504                 var index = arguments[i];
505 
506                 if (i == 3)
507                     index += offset;
508 
509                 if (index == -1)  // then walk up the tree
510                     parent = parent.parentNode;
511                 else
512                     parent = parent.childNodes[index];
513 
514                 if (FBTrace.DBG_DOMPLATE && !parent)
515                     FBTrace.sysout("domplate.__path__ will return null for root "+root+
516                         " and offset "+offset+" arguments["+i+"]="+arguments[i]+' index: '+
517                         index, {root: root});
518             }
519 
520             return parent;
521         }
522 
523         if (FBTrace.DBG_DOMPLATE)
524             fnBlock.push("//@ sourceURL=chrome://firebug/compileDOM_"+
525                 (this.tagName?this.tagName:'')+"_"+(uid++)+".js\n");
526 
527         var js = fnBlock.join("");
528         // Exceptions on this line are often in the eval
529         try
530         {
531             this.renderDOM = eval(js);
532         }
533         catch(exc)
534         {
535             if (FBTrace.DBG_DOMPLATE)
536                 FBTrace.sysout("renderDOM FAILS "+exc, {exc:exc, js: js});
537             var chained =  new Error("Domplate.renderDom FAILS");
538             chained.cause = {exc:exc, js: js};
539             throw chained;
540         }
541     },
542 
543     generateDOM: function(path, blocks, args)
544     {
545         if (this.listeners || this.props)
546             this.generateNodePath(path, blocks);
547 
548         if (this.listeners)
549         {
550             for (var i = 0; i < this.listeners.length; i += 2)
551             {
552                 var val = this.listeners[i+1];
553                 var arg = generateArg(val, path, args);
554 
555                 blocks.push('node.addEventListener("', this.listeners[i],
556                     '", __bind__(this, ', arg, '), false);\n');
557             }
558         }
559 
560         if (this.props)
561         {
562             for (var name in this.props)
563             {
564                 var val = this.props[name];
565                 var arg = generateArg(val, path, args);
566                 blocks.push('node.', name, ' = ', arg, ';\n');
567             }
568         }
569 
570         this.generateChildDOM(path, blocks, args);
571         return 1;
572     },
573 
574     generateNodePath: function(path, blocks)
575     {
576         blocks.push("var node = __path__(root, o");
577 
578         // this will be a sum of integers as a string which will be summed in the eval,
579         // then passed to __path__
580         for (var i = 0; i < path.length; ++i)
581             blocks.push(",", path[i]);
582 
583         blocks.push(");\n");
584 
585         if (FBTrace.DBG_DOMPLATE)
586         {
587             var nBlocks = 2*path.length + 2;
588             var genTrace = "FBTrace.sysout(\'"+blocks.slice(-nBlocks).join("").replace("\n","")+
589                 "\'+'->'+(node?node.outerHTML:'null'), node);\n";
590             blocks.push(genTrace);
591         }
592     },
593 
594     generateChildDOM: function(path, blocks, args)
595     {
596         path.push(0);
597         for (var i = 0; i < this.children.length; ++i)
598         {
599             var child = this.children[i];
600             if (isTag(child))
601                 path[path.length-1] += '+' + child.tag.generateDOM(path, blocks, args);
602             else
603                 path[path.length-1] += '+1';
604         }
605         path.pop();
606     },
607 
608     /**
609      * We are just hiding from javascript.options.strict. For some reasons it's ok if
610      * we return undefined here.
611      *
612      * @return null or undefined or possibly a context.
613      */
614     getContext: function()
615     {
616         return this.context;
617     }
618 };
619 
620 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
621 
622 DomplateEmbed.prototype = copyObject(DomplateTag.prototype,
623 {
624     merge: function(args, oldTag)
625     {
626         this.value = oldTag ? oldTag.value : parseValue(args[0]);
627         this.attrs = oldTag ? oldTag.attrs : {};
628         this.vars = oldTag ? copyArray(oldTag.vars) : [];
629 
630         var attrs = args[1];
631         for (var name in attrs)
632         {
633             var val = parseValue(attrs[name]);
634             this.attrs[name] = val;
635             readPartNames(val, this.vars);
636         }
637 
638         return creator(this, DomplateEmbed);
639     },
640 
641     getVarNames: function(names)
642     {
643         if (this.value instanceof Parts)
644             names.push(this.value.parts[0].name);
645 
646         if (this.vars)
647             names.push.apply(names, this.vars);
648     },
649 
650     generateMarkup: function(topBlock, topOuts, blocks, info)
651     {
652         this.addCode(topBlock, topOuts, blocks);
653 
654         if (FBTrace.DBG_DOMPLATE)
655             var beginBlock = blocks.length;
656 
657         blocks.push('__link__(');
658         addParts(this.value, '', blocks, info);
659         blocks.push(', __code__, __out__, {\n');
660 
661         var lastName = null;
662         for (var name in this.attrs)
663         {
664             if (lastName)
665                 blocks.push(',');
666             lastName = name;
667 
668             var val = this.attrs[name];
669             blocks.push('"', name, '":');
670             addParts(val, '', blocks, info);
671         }
672 
673         blocks.push('});\n');
674 
675         if (FBTrace.DBG_DOMPLATE)
676         {
677             FBTrace.sysout("DomplateEmbed.generateMarkup "+blocks.slice( - blocks.length +
678                 beginBlock).join("").replace("\n"," "), {value: this.value, attrs: this.attrs});
679         }
680 
681         //this.generateChildMarkup(topBlock, topOuts, blocks, info);
682     },
683 
684     generateDOM: function(path, blocks, args)  // XXXjjb args not used?
685     {
686         if (FBTrace.DBG_DOMPLATE)
687             var beginBlock = blocks.length;
688 
689         var embedName = 'e'+path.embedIndex++;
690 
691         this.generateNodePath(path, blocks);
692 
693         var valueName = 'd' + path.renderIndex++;
694         var argsName = 'd' + path.renderIndex++;
695         blocks.push(embedName + ' = __link__(node, ', valueName, ', ', argsName, ');\n');
696 
697         if (FBTrace.DBG_DOMPLATE)
698         {
699             FBTrace.sysout("DomplateEmbed.generateDOM "+blocks.slice( - blocks.length +
700                 beginBlock).join("").replace("\n"," "), {path: path});
701 
702             blocks.push("FBTrace.sysout('__link__ called with node:'+" +
703                 "node.outerHTML, node);\n");
704         }
705 
706         return embedName;
707     }
708 });
709 
710 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
711 
712 DomplateLoop.prototype = copyObject(DomplateTag.prototype,
713 {
714     merge: function(args, oldTag)
715     {
716         this.isLoop = true;
717         this.varName = oldTag ? oldTag.varName : args[0];
718         this.iter = oldTag ? oldTag.iter : parseValue(args[1]);
719         this.vars = [];
720 
721         this.children = oldTag ? copyArray(oldTag.children) : [];
722 
723         var offset = Math.min(args.length, 2);
724         parseChildren(args, offset, this.vars, this.children);
725 
726         return creator(this, DomplateLoop);
727     },
728 
729     getVarNames: function(names)
730     {
731         if (this.iter instanceof Parts)
732             names.push(this.iter.parts[0].name);
733 
734         DomplateTag.prototype.getVarNames.apply(this, [names]);
735     },
736 
737     generateMarkup: function(topBlock, topOuts, blocks, info)
738     {
739         this.addCode(topBlock, topOuts, blocks);
740 
741         // We are in a FOR loop and our this.iter property contains
742         // either a simple function name as a string or a Parts object
743         // with only ONE Variables object. There is only one variables object
744         // as the FOR argument can contain only ONE valid function callback
745         // with optional arguments or just one variable. Allowed arguments are
746         // func or $var or $var.sub or $var|func or $var1,$var2|func or $var|func1|func2 or $var1,$var2|func1|func2
747         var iterName;
748         if (this.iter instanceof Parts)
749         {
750             // We have a function with optional aruments or just one variable
751             var part = this.iter.parts[0];
752 
753             // Join our function arguments or variables
754             // If the user has supplied multiple variables without a function
755             // this will create an invalid result and we should probably add an
756             // error message here or just take the first variable
757             iterName = part.names.join(",");
758 
759             // Nest our functions
760             if (part.format)
761             {
762                 for (var i = 0; i < part.format.length; ++i)
763                     iterName = part.format[i] + "(" + iterName + ")";
764             }
765         }
766         else
767         {
768             // We have just a simple function name without any arguments
769             iterName = this.iter;
770         }
771 
772         blocks.push('__loop__.apply(this, [', iterName, ', __out__, function(',
773             this.varName, ', __out__) {\n');
774         this.generateChildMarkup(topBlock, topOuts, blocks, info);
775         this.addCode(topBlock, topOuts, blocks);
776 
777         blocks.push('}]);\n');
778     },
779 
780     generateDOM: function(path, blocks, args)
781     {
782         var iterName = 'd'+path.renderIndex++;
783         var counterName = 'i'+path.loopIndex;
784         var loopName = 'l'+path.loopIndex++;
785 
786         if (!path.length)
787             path.push(-1, 0);
788 
789         var preIndex = path.renderIndex;
790         path.renderIndex = 0;
791 
792         var nodeCount = 0;
793 
794         var subBlocks = [];
795         var basePath = path[path.length-1];
796         for (var i = 0; i < this.children.length; ++i)
797         {
798             path[path.length-1] = basePath+'+'+loopName+'+'+nodeCount;
799 
800             var child = this.children[i];
801             if (isTag(child))
802                 nodeCount += '+' + child.tag.generateDOM(path, subBlocks, args);
803             else
804                 nodeCount += '+1';
805         }
806 
807         path[path.length-1] = basePath+'+'+loopName;
808 
809         blocks.push(loopName,' = __loop__.apply(this, [', iterName, ', function(',
810             counterName,',',loopName);
811 
812         for (var i = 0; i < path.renderIndex; ++i)
813             blocks.push(',d'+i);
814 
815         blocks.push(') {\n');
816         blocks.push(subBlocks.join(""));
817         blocks.push('return ', nodeCount, ';\n');
818         blocks.push('}]);\n');
819 
820         path.renderIndex = preIndex;
821 
822         return loopName;
823     }
824 });
825 
826 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
827 
828 function Variables(names, format)
829 {
830     this.names = names;
831     this.format = format;
832 }
833 
834 function Parts(parts)
835 {
836     this.parts = parts;
837 }
838 
839 // ********************************************************************************************* //
840 
841 function parseParts(str)
842 {
843     // Match $var or $var.sub or $var|func or $var1,$var2|func or $var|func1|func2 or $var1,$var2|func1|func2
844     var re = /\$([_A-Za-z][_A-Za-z0-9.]*(,\$[_A-Za-z][_A-Za-z0-9.]*)*([_A-Za-z0-9.|]*))/g;
845     var index = 0;
846     var parts = [];
847 
848     var m;
849     while (m = re.exec(str))
850     {
851         var pre = str.substr(index, (re.lastIndex-m[0].length)-index);
852         if (pre)
853             parts.push(pre);
854 
855         var segs = m[1].split("|");
856         var vars = segs[0].split(",$");
857 
858         // Assemble the variables object and append to buffer
859         parts.push(new Variables(vars, segs.slice(1)));
860 
861         index = re.lastIndex;
862     }
863 
864     // No matches found at all so we return the whole string
865     if (!index)
866         return str;
867 
868     // If we have data after our last matched index we append it here as the final step
869     var post = str.substr(index);
870     if (post)
871         parts.push(post);
872 
873     return new Parts(parts);
874 }
875 
876 function parseValue(val)
877 {
878     return typeof(val) == 'string' ? parseParts(val) : val;
879 }
880 
881 function parseChildren(args, offset, vars, children)
882 {
883     for (var i = offset; i < args.length; ++i)
884     {
885         var val = parseValue(args[i]);
886         children.push(val);
887         readPartNames(val, vars);
888     }
889 }
890 
891 function readPartNames(val, vars)
892 {
893     if (val instanceof Parts)
894     {
895         for (var i = 0; i < val.parts.length; ++i)
896         {
897             var part = val.parts[i];
898             if (part instanceof Variables)
899                 vars.push(part.names[0]);
900         }
901     }
902 }
903 
904 function generateArg(val, path, args)
905 {
906     if (val instanceof Parts)
907     {
908         var vals = [];
909         for (var i = 0; i < val.parts.length; ++i)
910         {
911             var part = val.parts[i];
912             if (part instanceof Variables)
913             {
914                 var varName = 'd'+path.renderIndex++;
915                 if (part.format)
916                 {
917                     for (var j = 0; j < part.format.length; ++j)
918                         varName = part.format[j] + '(' + varName + ')';
919                 }
920 
921                 vals.push(varName);
922             }
923             else
924                 vals.push('"'+part.replace(/"/g, '\\"')+'"');
925         }
926 
927         return vals.join('+');
928     }
929     else
930     {
931         args.push(val);
932         return 's' + path.staticIndex++;
933     }
934 }
935 
936 function addParts(val, delim, block, info, escapeIt)
937 {
938     var vals = [];
939     if (val instanceof Parts)
940     {
941         for (var i = 0; i < val.parts.length; ++i)
942         {
943             var part = val.parts[i];
944             if (part instanceof Variables)
945             {
946                 var partName = part.names.join(",");
947                 if (part.format)
948                 {
949                     for (var j = 0; j < part.format.length; ++j)
950                         partName = part.format[j] + "(" + partName + ")";
951                 }
952 
953                 if (escapeIt)
954                     vals.push("__escape__(" + partName + ")");
955                 else
956                     vals.push(partName);
957             }
958             else
959                 vals.push('"'+ part + '"');
960         }
961     }
962     else if (isTag(val))
963     {
964         info.args.push(val);
965         vals.push('s'+info.argIndex++);
966     }
967     else
968         vals.push('"'+ val + '"');
969 
970     var parts = vals.join(delim);
971     if (parts)
972         block.push(delim, parts);
973 }
974 
975 function isTag(obj)
976 {
977     return (typeof(obj) == "function" || obj instanceof Function) && !!obj.tag;
978 }
979 
980 function creator(tag, cons)
981 {
982     var fn = function()
983     {
984         var tag = arguments.callee.tag;
985         var cons = arguments.callee.cons;
986         var newTag = new cons();
987         return newTag.merge(arguments, tag);
988     }
989 
990     fn.tag = tag;
991     fn.cons = cons;
992     extend(fn, Renderer);
993 
994     return fn;
995 }
996 
997 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
998 
999 function copyArray(oldArray)
1000 {
1001     var ary = [];
1002     if (oldArray)
1003         for (var i = 0; i < oldArray.length; ++i)
1004             ary.push(oldArray[i]);
1005    return ary;
1006 }
1007 
1008 function copyObject(l, r)
1009 {
1010     var m = {};
1011     extend(m, l);
1012     extend(m, r);
1013     return m;
1014 }
1015 
1016 function extend(l, r)
1017 {
1018     for (var n in r)
1019         l[n] = r[n];
1020 }
1021 
1022 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * //
1023 
1024 function ArrayIterator(array)
1025 {
1026     var index = -1;
1027 
1028     this.next = function()
1029     {
1030         if (++index >= array.length)
1031             throw StopIteration;
1032 
1033         return array[index];
1034     };
1035 }
1036 
1037 function StopIteration() {}
1038 
1039 // ********************************************************************************************* //
1040 
1041 var Renderer =
1042 {
1043     renderHTML: function(args, outputs, self)
1044     {
1045         try
1046         {
1047             var code = [];
1048             var markupArgs = [code, this.tag.getContext(), args, outputs];
1049             markupArgs.push.apply(markupArgs, this.tag.markupArgs);
1050             this.tag.renderMarkup.apply(self ? self : this.tag.subject, markupArgs);
1051             return code.join("");
1052         }
1053         catch (e)
1054         {
1055             if (FBTrace.DBG_DOMPLATE || FBTrace.DBG_ERRORS)
1056                 FBTrace.sysout("domplate.renderHTML; EXCEPTION " + e,
1057                     {exc: e, render: this.tag.renderMarkup.toSource()});
1058         }
1059     },
1060 
1061     insertRows: function(args, before, self)
1062     {
1063         if (!args)
1064             args = {};
1065 
1066         this.tag.compile();
1067 
1068         var outputs = [];
1069         var html = this.renderHTML(args, outputs, self);
1070 
1071         var doc = before.ownerDocument;
1072         var table = doc.createElement("table");
1073         table.innerHTML = html;
1074 
1075         var tbody = table.firstChild;
1076         var parent = before.localName.toLowerCase() == "tr" ? before.parentNode : before;
1077         var after = before.localName.toLowerCase() == "tr" ? before.nextSibling : null;
1078 
1079         var firstRow = tbody.firstChild, lastRow;
1080         while (tbody.firstChild)
1081         {
1082             lastRow = tbody.firstChild;
1083             if (after)
1084                 parent.insertBefore(lastRow, after);
1085             else
1086                 parent.appendChild(lastRow);
1087         }
1088 
1089         // To save the next poor soul:
1090         // In order to properly apply properties and event handlers on elements
1091         // constructed by a FOR tag, the tag needs to be able to iterate up and
1092         // down the tree. If FOR is the root element, as is the case with
1093         // many 'insertRows' calls, it will need to iterator over portions of the
1094         // new parent.
1095         //
1096         // To achieve this end, __path__ defines the -1 operator which allows
1097         // parent traversal. When combined with the offset that we calculate
1098         // below we are able to iterate over the elements.
1099         //
1100         // This fails when applied to a non-loop element as non-loop elements
1101         // do not generate to proper path to bounce up and down the tree.
1102         //
1103         var offset = 0;
1104         if (this.tag.isLoop)
1105         {
1106             var node = firstRow.parentNode.firstChild;
1107             for (; node && node != firstRow; node = node.nextSibling)
1108                 ++offset;
1109         }
1110 
1111         // strict warning: this.tag.context undefined
1112         var domArgs = [firstRow, this.tag.getContext(), offset];
1113         domArgs.push.apply(domArgs, this.tag.domArgs);
1114         domArgs.push.apply(domArgs, outputs);
1115 
1116         this.tag.renderDOM.apply(self ? self : this.tag.subject, domArgs);
1117         return [firstRow, lastRow];
1118     },
1119 
1120     insertBefore: function(args, before, self)
1121     {
1122         return this.insertNode(
1123                 args, before.ownerDocument,
1124                 function beforeInserter(frag) {
1125                     before.parentNode.insertBefore(frag, before);
1126                 },
1127                 self);
1128     },
1129 
1130     insertAfter: function(args, after, self)
1131     {
1132         return this.insertNode(
1133                 args, after.ownerDocument,
1134                 function(frag) {
1135                     after.parentNode.insertBefore(frag, after.nextSibling);
1136                 },
1137                 self);
1138     },
1139 
1140     insertNode: function(args, doc, inserter, self)
1141     {
1142         if (!args)
1143             args = {};
1144 
1145         this.tag.compile();
1146 
1147         var outputs = [];
1148         var html = this.renderHTML(args, outputs, self);
1149         if (FBTrace.DBG_DOMPLATE)
1150             FBTrace.sysout("domplate.insertNode html: "+html+"\n");
1151 
1152         var range = doc.createRange();
1153 
1154         // if doc starts with a Text node, domplate fails because the fragment starts
1155         // with a text node. That must be a gecko bug, but let's just workaround it since
1156         // we want to switch to innerHTML anyway
1157         var aDiv = doc.getElementsByTagName("div").item(0);
1158         range.setStartBefore(aDiv);
1159 
1160         // TODO replace with standard innerHTML
1161         var frag = range.createContextualFragment(html);
1162 
1163         var root = frag.firstChild;
1164         root = inserter(frag) || root;
1165 
1166         var domArgs = [root, this.tag.context, 0];
1167         domArgs.push.apply(domArgs, this.tag.domArgs);
1168         domArgs.push.apply(domArgs, outputs);
1169 
1170         if (FBTrace.DBG_DOMPLATE)
1171             FBTrace.sysout("domplate.insertNode domArgs:", domArgs);
1172         this.tag.renderDOM.apply(self ? self : this.tag.subject, domArgs);
1173 
1174         return root;
1175     },
1176 
1177     replace: function(args, parent, self)
1178     {
1179         if (!args)
1180             args = {};
1181 
1182         this.tag.compile();
1183 
1184         var outputs = [];
1185         var html = this.renderHTML(args, outputs, self);
1186 
1187         var root;
1188         if (parent.nodeType == Node.ELEMENT_NODE)
1189         {
1190             parent.innerHTML = html;
1191             root = parent.firstChild;
1192         }
1193         else
1194         {
1195             if (!parent || parent.nodeType != Node.DOCUMENT_NODE)
1196                 parent = document;
1197 
1198             if (!womb || womb.ownerDocument != parent)
1199                 womb = parent.createElement("div");
1200             womb.innerHTML = html;
1201 
1202             root = womb.firstChild;
1203             //womb.removeChild(root);
1204         }
1205 
1206         var domArgs = [root, this.tag.context, 0];
1207         domArgs.push.apply(domArgs, this.tag.domArgs);
1208         domArgs.push.apply(domArgs, outputs);
1209 
1210         try
1211         {
1212             this.tag.renderDOM.apply(self ? self : this.tag.subject, domArgs);
1213         }
1214         catch(exc)
1215         {
1216             if (FBTrace.DBG_ERRORS)
1217             {
1218                 FBTrace.sysout("domplate renderDom FAILS " + exc, {exc: exc, renderDOM:
1219                     this.tag.renderDOM.toSource(), domplate: this, domArgs: domArgs, self: self});
1220             }
1221 
1222             var chained =  new Error("Domplate.renderDom FAILS: "+exc);
1223             chained.cause = {exc: exc, renderDOM: this.tag.renderDOM.toSource(),
1224                 domplate: this, domArgs: domArgs, self: self};
1225 
1226             throw chained;
1227         }
1228 
1229         return root;
1230     },
1231 
1232     append: function(args, parent, self)
1233     {
1234         if (!args)
1235             args = {};
1236 
1237         this.tag.compile();
1238 
1239         var outputs = [];
1240         var html = this.renderHTML(args, outputs, self);
1241         if (FBTrace.DBG_DOMPLATE)
1242             FBTrace.sysout("domplate.append html: "+html+"\n");
1243 
1244         if (!womb || womb.ownerDocument != parent.ownerDocument)
1245             womb = parent.ownerDocument.createElement("div");
1246         womb.innerHTML = html;
1247 
1248         var root = womb.firstChild;
1249         while (womb.firstChild)
1250             parent.appendChild(womb.firstChild);
1251 
1252         var domArgs = [root, this.tag.context, 0];
1253         domArgs.push.apply(domArgs, this.tag.domArgs);
1254         domArgs.push.apply(domArgs, outputs);
1255 
1256         if (FBTrace.DBG_DOMPLATE)
1257             FBTrace.sysout("domplate.append domArgs:", domArgs);
1258 
1259         this.tag.renderDOM.apply(self ? self : this.tag.subject, domArgs);
1260 
1261         return root;
1262     }
1263 };
1264 
1265 // ********************************************************************************************* //
1266 
1267 function defineTags()
1268 {
1269     for (var i = 0; i < arguments.length; ++i)
1270     {
1271         var tagName = arguments[i];
1272         var fn = createTagHandler(tagName);
1273         var fnName = tagName.toUpperCase();
1274 
1275         Domplate[fnName] = fn;
1276     }
1277 
1278     function createTagHandler(tagName)
1279     {
1280         return function() {
1281             var newTag = new Domplate.DomplateTag(tagName);
1282             return newTag.merge(arguments);
1283         }
1284     }
1285 }
1286 
1287 defineTags(
1288     "a", "button", "br", "canvas", "col", "colgroup", "div", "fieldset", "form", "h1", "h2",
1289     "h3", "hr", "img", "input", "label", "legend", "li", "ol", "optgroup", "option", "p",
1290     "pre", "select", "b", "span", "strong", "table", "tbody", "td", "textarea", "tfoot", "th",
1291     "thead", "tr", "tt", "ul", "iframe", "code", "style",
1292 
1293     // HTML5
1294     "article", "aside", "audio", "bb", "canvas", "command", "datagrid", "datalist", "details",
1295     "dialog", "embed", "eventsource", "figure", "footer", "keygen", "mark", "meter", "nav",
1296     "output", "progress", "ruby", "rp", "rt", "section", "source", "time", "video"
1297 );
1298 
1299 // ********************************************************************************************* //
1300 // Registration
1301 
1302 return Domplate;
1303 
1304 // ********************************************************************************************* //
1305 });
1306