1 /* See license.txt for terms of usage */
  2 
  3 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
  4 
  5 function DomplateTag(tagName)
  6 {
  7     this.tagName = tagName;
  8 }
  9 
 10 function DomplateEmbed()
 11 {
 12 }
 13 
 14 function DomplateLoop()
 15 {
 16 }
 17 
 18 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
 19 
 20 (function() {
 21 
 22 var womb = null;
 23 
 24 top.domplate = function()
 25 {
 26     var lastSubject;
 27     for (var i = 0; i < arguments.length; ++i)
 28         lastSubject = lastSubject ? copyObject(lastSubject, arguments[i]) : arguments[i];
 29 
 30     for (var name in lastSubject)
 31     {
 32         var val = lastSubject[name];
 33         if (isTag(val))
 34             val.tag.subject = lastSubject;
 35     }
 36 
 37     return lastSubject;
 38 };
 39 
 40 domplate.context = function(context, fn)
 41 {
 42     var lastContext = domplate.lastContext;
 43     domplate.topContext = context;
 44     fn.apply(context);
 45     domplate.topContext = lastContext;
 46 };
 47 
 48 FBL.TAG = function()
 49 {
 50     var embed = new DomplateEmbed();
 51     return embed.merge(arguments);
 52 };
 53 
 54 FBL.FOR = function()
 55 {
 56     var loop = new DomplateLoop();
 57     return loop.merge(arguments);
 58 };
 59 
 60 DomplateTag.prototype =
 61 {
 62     /*
 63      *  Initializer for DOM templates. Called to create new Functions objects like TR, TD, OBJLINK, etc. See defineTag
 64      *  @param args keyword argments for the template, the {} brace stuff after the tag name, eg TR({...}, TD(...
 65      *  @param oldTag a nested tag, eg the TD tag in TR({...}, TD(...
 66      */
 67     merge: function(args, oldTag)
 68     {
 69         if (oldTag)
 70             this.tagName = oldTag.tagName;
 71 
 72         this.context = oldTag ? oldTag.context : null;  // normally null on construction
 73         this.subject = oldTag ? oldTag.subject : null;
 74         this.attrs = oldTag ? copyObject(oldTag.attrs) : {};
 75         this.classes = oldTag ? copyObject(oldTag.classes) : {};
 76         this.props = oldTag ? copyObject(oldTag.props) : null;
 77         this.listeners = oldTag ? copyArray(oldTag.listeners) : null;
 78         this.children = oldTag ? copyArray(oldTag.children) : [];
 79         this.vars = oldTag ? copyArray(oldTag.vars) : [];
 80 
 81         var attrs = args.length ? args[0] : null;
 82         var hasAttrs = typeof(attrs) == "object" && !isTag(attrs);
 83 
 84         // Do not clear children, they can be copied from the oldTag.
 85         //this.children = [];
 86 
 87         if (domplate.topContext)
 88             this.context = domplate.topContext;
 89 
 90         if (args.length)
 91             parseChildren(args, hasAttrs ? 1 : 0, this.vars, this.children);
 92 
 93         if (hasAttrs)
 94             this.parseAttrs(attrs);
 95 
 96         return creator(this, DomplateTag);
 97     },
 98 
 99     parseAttrs: function(args)
100     {
101         for (var name in args)
102         {
103             var val = parseValue(args[name]);
104             readPartNames(val, this.vars);
105 
106             if (name.indexOf("on") == 0)
107             {
108                 var eventName = name.substr(2);
109                 if (!this.listeners)
110                     this.listeners = [];
111                 this.listeners.push(eventName, val);
112             }
113             else if (name[0] == "_")
114             {
115                 var propName = name.substr(1);
116                 if (!this.props)
117                     this.props = {};
118                 this.props[propName] = val;
119             }
120             else if (name[0] == "$")
121             {
122                 var className = name.substr(1);
123                 if (!this.classes)
124                     this.classes = {};
125                 this.classes[className] = val;
126             }
127             else
128             {
129                 if (name == "class" && this.attrs.hasOwnProperty(name) )
130                     this.attrs[name] += " " + val;
131                 else
132                     this.attrs[name] = val;
133             }
134         }
135     },
136 
137     compile: function()
138     {
139         if (this.renderMarkup)
140             return;
141 
142         this.compileMarkup();
143         this.compileDOM();
144     },
145 
146     compileMarkup: function()
147     {
148         this.markupArgs = [];
149         var topBlock = [], topOuts = [], blocks = [], info = {args: this.markupArgs, argIndex: 0};
150 
151         this.generateMarkup(topBlock, topOuts, blocks, info);
152         this.addCode(topBlock, topOuts, blocks);
153 
154         var fnBlock = ['(function (__code__, __context__, __in__, __out__'];
155         for (var i = 0; i < info.argIndex; ++i)
156             fnBlock.push(', s', i);
157         fnBlock.push(') {\n');
158 
159         if (this.subject)
160             fnBlock.push('with (this) {\n');
161         if (this.context)
162             fnBlock.push('with (__context__) {\n');
163         fnBlock.push('with (__in__) {\n');
164 
165         fnBlock.push.apply(fnBlock, blocks);
166 
167         if (this.subject)
168             fnBlock.push('}\n');
169         if (this.context)
170             fnBlock.push('}\n');
171 
172         fnBlock.push('}})\n');
173 
174         function __link__(tag, code, outputs, args)
175         {
176             if (!tag || !tag.tag)
177                 return;
178 
179             tag.tag.compile();
180 
181             var tagOutputs = [];
182             var markupArgs = [code, tag.tag.context, args, tagOutputs];
183             markupArgs.push.apply(markupArgs, tag.tag.markupArgs);
184             tag.tag.renderMarkup.apply(tag.tag.subject, markupArgs);
185 
186             outputs.push(tag);
187             outputs.push(tagOutputs);
188         }
189 
190         function __escape__(value)
191         {
192             function replaceChars(ch)
193             {
194                 switch (ch)
195                 {
196                     case "<":
197                         return "<";
198                     case ">":
199                         return ">";
200                     case "&":
201                         return "&";
202                     case "'":
203                         return "'";
204                     case '"':
205                         return """;
206                 }
207                 return "?";
208             };
209             return String(value).replace(/[<>&"']/g, replaceChars);
210         }
211 
212         function __loop__(iter, outputs, fn)
213         {
214             var iterOuts = [];
215             outputs.push(iterOuts);
216 
217             if (iter instanceof Array)
218                 iter = new ArrayIterator(iter);
219 
220             try
221             {
222                 while (1)
223                 {
224                     var value = iter.next();
225                     var itemOuts = [0,0];
226                     iterOuts.push(itemOuts);
227                     fn.apply(this, [value, itemOuts]);
228                 }
229             }
230             catch (exc)
231             {
232                 if (exc != StopIteration)
233                     throw exc;
234             }
235         }
236 
237         var js = fnBlock.join("");
238         this.renderMarkup = eval(js);
239     },
240 
241     getVarNames: function(args)
242     {
243         if (this.vars)
244             args.push.apply(args, this.vars);
245 
246         for (var i = 0; i < this.children.length; ++i)
247         {
248             var child = this.children[i];
249             if (isTag(child))
250                 child.tag.getVarNames(args);
251             else if (child instanceof Parts)
252             {
253                 for (var i = 0; i < child.parts.length; ++i)
254                 {
255                     if (child.parts[i] instanceof Variable)
256                     {
257                         var name = child.parts[i].name;
258                         var names = name.split(".");
259                         args.push(names[0]);
260                     }
261                 }
262             }
263         }
264     },
265 
266     generateMarkup: function(topBlock, topOuts, blocks, info)
267     {
268         topBlock.push(',"<', this.tagName, '"');
269 
270         for (var name in this.attrs)
271         {
272             if (name != "class")
273             {
274                 var val = this.attrs[name];
275                 topBlock.push(', " ', name, '=\\""');
276                 addParts(val, ',', topBlock, info, true);
277                 topBlock.push(', "\\""');
278             }
279         }
280 
281         if (this.listeners)
282         {
283             for (var i = 0; i < this.listeners.length; i += 2)
284                 readPartNames(this.listeners[i+1], topOuts);
285         }
286 
287         if (this.props)
288         {
289             for (var name in this.props)
290                 readPartNames(this.props[name], topOuts);
291         }
292 
293         if ( this.attrs.hasOwnProperty("class") || this.classes)
294         {
295             topBlock.push(', " class=\\""');
296             if (this.attrs.hasOwnProperty("class"))
297                 addParts(this.attrs["class"], ',', topBlock, info, true);
298               topBlock.push(', " "');
299             for (var name in this.classes)
300             {
301                 topBlock.push(', (');
302                 addParts(this.classes[name], '', topBlock, info);
303                 topBlock.push(' ? "', name, '" + " " : "")');
304             }
305             topBlock.push(', "\\""');
306         }
307         topBlock.push(',">"');
308 
309         this.generateChildMarkup(topBlock, topOuts, blocks, info);
310         topBlock.push(',"</', this.tagName, '>"');
311         if (FBTrace.DBG_DOMPLATE)
312             FBTrace.sysout("generateMarkup: "+this.tagName, topBlock.join(""));
313     },
314 
315     generateChildMarkup: function(topBlock, topOuts, blocks, info)
316     {
317         for (var i = 0; i < this.children.length; ++i)
318         {
319             var child = this.children[i];
320             if (isTag(child))
321                 child.tag.generateMarkup(topBlock, topOuts, blocks, info);
322             else
323                 addParts(child, ',', topBlock, info, true);
324         }
325     },
326 
327     addCode: function(topBlock, topOuts, blocks)
328     {
329         if (topBlock.length)
330             blocks.push('__code__.push(""', topBlock.join(""), ');\n');
331         if (topOuts.length)
332             blocks.push('__out__.push(', topOuts.join(","), ');\n');
333         topBlock.splice(0, topBlock.length);
334         topOuts.splice(0, topOuts.length);
335     },
336 
337     addLocals: function(blocks)
338     {
339         var varNames = [];
340         this.getVarNames(varNames);
341 
342         var map = {};
343         for (var i = 0; i < varNames.length; ++i)
344         {
345             var name = varNames[i];
346             if ( map.hasOwnProperty(name) )
347                 continue;
348 
349             map[name] = 1;
350             var names = name.split(".");
351             blocks.push('var ', names[0] + ' = ' + '__in__.' + names[0] + ';\n');
352         }
353     },
354 
355     compileDOM: function()
356     {
357         var path = [];
358         var blocks = [];
359         this.domArgs = [];
360         path.embedIndex = 0;
361         path.loopIndex = 0;
362         path.staticIndex = 0;
363         path.renderIndex = 0;
364         var nodeCount = this.generateDOM(path, blocks, this.domArgs);
365 
366         var fnBlock = ['(function (root, context, o'];
367 
368         for (var i = 0; i < path.staticIndex; ++i)
369             fnBlock.push(', ', 's'+i);
370 
371         for (var i = 0; i < path.renderIndex; ++i)
372             fnBlock.push(', ', 'd'+i);
373 
374         fnBlock.push(') {\n');
375         for (var i = 0; i < path.loopIndex; ++i)
376             fnBlock.push('var l', i, ' = 0;\n');
377         for (var i = 0; i < path.embedIndex; ++i)
378             fnBlock.push('var e', i, ' = 0;\n');
379 
380         if (this.subject)
381             fnBlock.push('with (this) {\n');
382         if (this.context)
383             fnBlock.push('with (context) {\n');
384 
385         fnBlock.push(blocks.join(""));
386 
387         if (this.subject)
388             fnBlock.push('}\n');
389         if (this.context)
390             fnBlock.push('}\n');
391 
392         fnBlock.push('return ', nodeCount, ';\n');
393         fnBlock.push('})\n');
394 
395         function __bind__(object, fn)
396         {
397             return function(event) { return fn.apply(object, [event]); }
398         }
399 
400         function __link__(node, tag, args)
401         {
402             if (!tag || !tag.tag)
403                 return;
404 
405             tag.tag.compile();
406 
407             var domArgs = [node, tag.tag.context, 0];
408             domArgs.push.apply(domArgs, tag.tag.domArgs);
409             domArgs.push.apply(domArgs, args);
410 
411             return tag.tag.renderDOM.apply(tag.tag.subject, domArgs);
412         }
413 
414         var self = this;
415         function __loop__(iter, fn)
416         {
417             var nodeCount = 0;
418             for (var i = 0; i < iter.length; ++i)
419             {
420                 iter[i][0] = i;
421                 iter[i][1] = nodeCount;
422                 nodeCount += fn.apply(this, iter[i]);
423             }
424             return nodeCount;
425         }
426 
427         function __path__(parent, offset)
428         {
429             var root = parent;
430 
431             for (var i = 2; i < arguments.length; ++i)
432             {
433                 var index = arguments[i];
434                 if (i == 3)
435                     index += offset;
436 
437                 if (index == -1)
438                     parent = parent.parentNode;
439                 else
440                     parent = parent.childNodes[index];
441             }
442 
443             return parent;
444         }
445         var js = fnBlock.join("");
446         // Exceptions on this line are often in the eval
447         this.renderDOM = eval(js);
448     },
449 
450     generateDOM: function(path, blocks, args)
451     {
452         if (this.listeners || this.props)
453             this.generateNodePath(path, blocks);
454 
455         if (this.listeners)
456         {
457             for (var i = 0; i < this.listeners.length; i += 2)
458             {
459                 var val = this.listeners[i+1];
460                 var arg = generateArg(val, path, args);
461                 blocks.push('node.addEventListener("', this.listeners[i], '", __bind__(this, ', arg, '), false);\n');
462             }
463         }
464 
465         if (this.props)
466         {
467             for (var name in this.props)
468             {
469                 var val = this.props[name];
470                 var arg = generateArg(val, path, args);
471                 blocks.push('node.', name, ' = ', arg, ';\n');
472             }
473         }
474 
475         this.generateChildDOM(path, blocks, args);
476         return 1;
477     },
478 
479     generateNodePath: function(path, blocks)
480     {
481         blocks.push("var node = __path__(root, o");
482         for (var i = 0; i < path.length; ++i)
483             blocks.push(",", path[i]);
484         blocks.push(");\n");
485     },
486 
487     generateChildDOM: function(path, blocks, args)
488     {
489         path.push(0);
490         for (var i = 0; i < this.children.length; ++i)
491         {
492             var child = this.children[i];
493             if (isTag(child))
494                 path[path.length-1] += '+' + child.tag.generateDOM(path, blocks, args);
495             else
496                 path[path.length-1] += '+1';
497         }
498         path.pop();
499     },
500 
501     /*
502      * We are just hiding from javascript.options.strict. For some reasons it's ok if we return undefined here.
503      * @return null or undefined or possibly a context.
504      */
505     getContext: function()
506     {
507         return this.context;
508     }
509 };
510 
511 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
512 
513 DomplateEmbed.prototype = copyObject(DomplateTag.prototype,
514 {
515     merge: function(args, oldTag)
516     {
517         this.value = oldTag ? oldTag.value : parseValue(args[0]);
518         this.attrs = oldTag ? oldTag.attrs : {};
519         this.vars = oldTag ? copyArray(oldTag.vars) : [];
520 
521         var attrs = args[1];
522         for (var name in attrs)
523         {
524             var val = parseValue(attrs[name]);
525             this.attrs[name] = val;
526             readPartNames(val, this.vars);
527         }
528 
529         return creator(this, DomplateEmbed);
530     },
531 
532     getVarNames: function(names)
533     {
534         if (this.value instanceof Parts)
535             names.push(this.value.parts[0].name);
536 
537         if (this.vars)
538             names.push.apply(names, this.vars);
539     },
540 
541     generateMarkup: function(topBlock, topOuts, blocks, info)
542     {
543         this.addCode(topBlock, topOuts, blocks);
544 
545         blocks.push('__link__(');
546         addParts(this.value, '', blocks, info);
547         blocks.push(', __code__, __out__, {\n');
548 
549         var lastName = null;
550         for (var name in this.attrs)
551         {
552             if (lastName)
553                 blocks.push(',');
554             lastName = name;
555 
556             var val = this.attrs[name];
557             blocks.push('"', name, '":');
558             addParts(val, '', blocks, info);
559         }
560 
561         blocks.push('});\n');
562         //this.generateChildMarkup(topBlock, topOuts, blocks, info);
563     },
564 
565     generateDOM: function(path, blocks, args)
566     {
567         var embedName = 'e'+path.embedIndex++;
568 
569         this.generateNodePath(path, blocks);
570 
571         var valueName = 'd' + path.renderIndex++;
572         var argsName = 'd' + path.renderIndex++;
573         blocks.push(embedName + ' = __link__(node, ', valueName, ', ', argsName, ');\n');
574 
575         return embedName;
576     }
577 });
578 
579 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
580 
581 DomplateLoop.prototype = copyObject(DomplateTag.prototype,
582 {
583     merge: function(args, oldTag)
584     {
585         this.isLoop = true;
586         this.varName = oldTag ? oldTag.varName : args[0];
587         this.iter = oldTag ? oldTag.iter : parseValue(args[1]);
588         this.vars = [];
589 
590         this.children = oldTag ? copyArray(oldTag.children) : [];
591 
592         var offset = Math.min(args.length, 2);
593         parseChildren(args, offset, this.vars, this.children);
594 
595         return creator(this, DomplateLoop);
596     },
597 
598     getVarNames: function(names)
599     {
600         if (this.iter instanceof Parts)
601             names.push(this.iter.parts[0].name);
602 
603         DomplateTag.prototype.getVarNames.apply(this, [names]);
604     },
605 
606     generateMarkup: function(topBlock, topOuts, blocks, info)
607     {
608         this.addCode(topBlock, topOuts, blocks);
609 
610         var iterName;
611         if (this.iter instanceof Parts)
612         {
613             var part = this.iter.parts[0];
614             iterName = part.name;
615 
616             if (part.format)
617             {
618                 for (var i = 0; i < part.format.length; ++i)
619                     iterName = part.format[i] + "(" + iterName + ")";
620             }
621         }
622         else
623             iterName = this.iter;
624 
625         blocks.push('__loop__.apply(this, [', iterName, ', __out__, function(', this.varName, ', __out__) {\n');
626         this.generateChildMarkup(topBlock, topOuts, blocks, info);
627         this.addCode(topBlock, topOuts, blocks);
628         blocks.push('}]);\n');
629     },
630 
631     generateDOM: function(path, blocks, args)
632     {
633         var iterName = 'd'+path.renderIndex++;
634         var counterName = 'i'+path.loopIndex;
635         var loopName = 'l'+path.loopIndex++;
636 
637         if (!path.length)
638             path.push(-1, 0);
639 
640         var preIndex = path.renderIndex;
641         path.renderIndex = 0;
642 
643         var nodeCount = 0;
644 
645         var subBlocks = [];
646         var basePath = path[path.length-1];
647         for (var i = 0; i < this.children.length; ++i)
648         {
649             path[path.length-1] = basePath+'+'+loopName+'+'+nodeCount;
650 
651             var child = this.children[i];
652             if (isTag(child))
653                 nodeCount += '+' + child.tag.generateDOM(path, subBlocks, args);
654             else
655                 nodeCount += '+1';
656         }
657 
658         path[path.length-1] = basePath+'+'+loopName;
659 
660         blocks.push(loopName,' = __loop__.apply(this, [', iterName, ', function(', counterName,',',loopName);
661         for (var i = 0; i < path.renderIndex; ++i)
662             blocks.push(',d'+i);
663         blocks.push(') {\n');
664         blocks.push(subBlocks.join(""));
665         blocks.push('return ', nodeCount, ';\n');
666         blocks.push('}]);\n');
667 
668         path.renderIndex = preIndex;
669 
670         return loopName;
671     }
672 });
673 
674 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
675 
676 function Variable(name, format)
677 {
678     this.name = name;
679     this.format = format;
680 }
681 
682 function Parts(parts)
683 {
684     this.parts = parts;
685 }
686 
687 // ************************************************************************************************
688 
689 function parseParts(str)
690 {
691     var re = /\$([_A-Za-z][_A-Za-z0-9.|]*)/g;
692     var index = 0;
693     var parts = [];
694 
695     var m;
696     while (m = re.exec(str))
697     {
698         var pre = str.substr(index, (re.lastIndex-m[0].length)-index);
699         if (pre)
700             parts.push(pre);
701 
702         var expr = m[1].split("|");
703         parts.push(new Variable(expr[0], expr.slice(1)));
704         index = re.lastIndex;
705     }
706 
707     if (!index)
708         return str;
709 
710     var post = str.substr(index);
711     if (post)
712         parts.push(post);
713 
714     return new Parts(parts);
715 }
716 
717 function parseValue(val)
718 {
719     return typeof(val) == 'string' ? parseParts(val) : val;
720 }
721 
722 function parseChildren(args, offset, vars, children)
723 {
724     for (var i = offset; i < args.length; ++i)
725     {
726         var val = parseValue(args[i]);
727         children.push(val);
728         readPartNames(val, vars);
729     }
730 }
731 
732 function readPartNames(val, vars)
733 {
734     if (val instanceof Parts)
735     {
736         for (var i = 0; i < val.parts.length; ++i)
737         {
738             var part = val.parts[i];
739             if (part instanceof Variable)
740                 vars.push(part.name);
741         }
742     }
743 }
744 
745 function generateArg(val, path, args)
746 {
747     if (val instanceof Parts)
748     {
749         var vals = [];
750         for (var i = 0; i < val.parts.length; ++i)
751         {
752             var part = val.parts[i];
753             if (part instanceof Variable)
754             {
755                 var varName = 'd'+path.renderIndex++;
756                 if (part.format)
757                 {
758                     for (var j = 0; j < part.format.length; ++j)
759                         varName = part.format[j] + '(' + varName + ')';
760                 }
761 
762                 vals.push(varName);
763             }
764             else
765                 vals.push('"'+part.replace(/"/g, '\\"')+'"');
766         }
767 
768         return vals.join('+');
769     }
770     else
771     {
772         args.push(val);
773         return 's' + path.staticIndex++;
774     }
775 }
776 
777 function addParts(val, delim, block, info, escapeIt)
778 {
779     var vals = [];
780     if (val instanceof Parts)
781     {
782         for (var i = 0; i < val.parts.length; ++i)
783         {
784             var part = val.parts[i];
785             if (part instanceof Variable)
786             {
787                 var partName = part.name;
788                 if (part.format)
789                 {
790                     for (var j = 0; j < part.format.length; ++j)
791                         partName = part.format[j] + "(" + partName + ")";
792                 }
793 
794                 if (escapeIt)
795                     vals.push("__escape__(" + partName + ")");
796                 else
797                     vals.push(partName);
798             }
799             else
800                 vals.push('"'+ part + '"');
801         }
802     }
803     else if (isTag(val))
804     {
805         info.args.push(val);
806         vals.push('s'+info.argIndex++);
807     }
808     else
809         vals.push('"'+ val + '"');
810 
811     var parts = vals.join(delim);
812     if (parts)
813         block.push(delim, parts);
814 }
815 
816 function isTag(obj)
817 {
818     return (typeof(obj) == "function" || obj instanceof Function) && !!obj.tag;
819 }
820 
821 function creator(tag, cons)
822 {
823     var fn = new Function(
824         "var tag = arguments.callee.tag;" +
825         "var cons = arguments.callee.cons;" +
826         "var newTag = new cons();" +
827         "return newTag.merge(arguments, tag);");
828 
829     fn.tag = tag;
830     fn.cons = cons;
831     extend(fn, Renderer);
832 
833     return fn;
834 }
835 
836 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
837 
838 function copyArray(oldArray)
839 {
840     var ary = [];
841     if (oldArray)
842         for (var i = 0; i < oldArray.length; ++i)
843             ary.push(oldArray[i]);
844    return ary;
845 }
846 
847 function copyObject(l, r)
848 {
849     var m = {};
850     extend(m, l);
851     extend(m, r);
852     return m;
853 }
854 
855 function extend(l, r)
856 {
857     for (var n in r)
858         l[n] = r[n];
859 }
860 
861 // * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
862 
863 function ArrayIterator(array)
864 {
865     var index = -1;
866 
867     this.next = function()
868     {
869         if (++index >= array.length)
870             throw StopIteration;
871 
872         return array[index];
873     };
874 }
875 
876 function StopIteration() {}
877 
878 FBL.$break = function()
879 {
880     throw StopIteration;
881 };
882 
883 // ************************************************************************************************
884 
885 var Renderer =
886 {
887     renderHTML: function(args, outputs, self)
888     {
889         var code = [];
890         var markupArgs = [code, this.tag.getContext(), args, outputs];
891         markupArgs.push.apply(markupArgs, this.tag.markupArgs);
892         this.tag.renderMarkup.apply(self ? self : this.tag.subject, markupArgs);
893         return code.join("");
894     },
895 
896     insertRows: function(args, before, self)
897     {
898         if (!args)
899             args = {};
900 
901         this.tag.compile();
902 
903         var outputs = [];
904         var html = this.renderHTML(args, outputs, self);
905 
906         var doc = before.ownerDocument;
907         var table = doc.createElement("table");
908         table.innerHTML = html;
909 
910         var tbody = table.firstChild;
911         var parent = before.localName.toLowerCase() == "tr" ? before.parentNode : before;
912         var after = before.localName.toLowerCase() == "tr" ? before.nextSibling : null;
913 
914         var firstRow = tbody.firstChild, lastRow;
915         while (tbody.firstChild)
916         {
917             lastRow = tbody.firstChild;
918             if (after)
919                 parent.insertBefore(lastRow, after);
920             else
921                 parent.appendChild(lastRow);
922         }
923 
924         // To save the next poor soul:
925         // In order to properly apply properties and event handlers on elements
926         // constructed by a FOR tag, the tag needs to be able to iterate up and
927         // down the tree, meaning if FOR is the root element as is the case with
928         // many insertRows calls, it will need to iterator over portions of the
929         // new parent.
930         //
931         // To achieve this end, __path__ defines the -1 operator which allows
932         // parent traversal. When combined with the offset that we calculate
933         // below we are able to iterate over the elements.
934         //
935         // This fails when applied to a non-loop element as non-loop elements
936         // Do not generate to proper path to bounce up and down the tree.
937         var offset = 0;
938         if (this.tag.isLoop)
939         {
940             var node = firstRow.parentNode.firstChild;
941             for (; node && node != firstRow; node = node.nextSibling)
942                 ++offset;
943         }
944 
945         // strict warning: this.tag.context undefined
946         var domArgs = [firstRow, this.tag.getContext(), offset];
947         domArgs.push.apply(domArgs, this.tag.domArgs);
948         domArgs.push.apply(domArgs, outputs);
949 
950         this.tag.renderDOM.apply(self ? self : this.tag.subject, domArgs);
951         return [firstRow, lastRow];
952     },
953 
954     insertBefore: function(args, before, self)
955     {
956         return this.insertNode(
957                 args, before.ownerDocument,
958                 function(frag) {
959                     before.parentNode.insertBefore(frag, before);
960                 },
961                 self);
962     },
963 
964     insertAfter: function(args, after, self)
965     {
966         return this.insertNode(
967                 args, after.ownerDocument,
968                 function(frag) {
969                     after.parentNode.insertBefore(frag, after.nextSibling);
970                 },
971                 self);
972     },
973 
974     insertNode: function(args, doc, inserter, self)
975     {
976         if (!args)
977             args = {};
978 
979         this.tag.compile();
980 
981         var outputs = [];
982         var html = this.renderHTML(args, outputs, self);
983         if (FBTrace.DBG_DOMPLATE)
984             FBTrace.sysout("domplate.insertNode html: "+html+"\n");
985 
986         var range = doc.createRange();
987         range.selectNode(doc.body);
988         var frag = range.createContextualFragment(html);
989 
990         var root = frag.firstChild;
991         root = inserter(frag) || root;
992 
993         var domArgs = [root, this.tag.context, 0];
994         domArgs.push.apply(domArgs, this.tag.domArgs);
995         domArgs.push.apply(domArgs, outputs);
996 
997         if (FBTrace.DBG_DOMPLATE)
998             FBTrace.sysout("domplate.insertNode domArgs:", domArgs);
999         this.tag.renderDOM.apply(self ? self : this.tag.subject, domArgs);
1000 
1001         return root;
1002     },
1003 
1004     replace: function(args, parent, self)
1005     {
1006         if (!args)
1007             args = {};
1008 
1009         this.tag.compile();
1010 
1011         var outputs = [];
1012         var html = this.renderHTML(args, outputs, self);
1013 
1014         var root;
1015         if (parent.nodeType == 1)
1016         {
1017             parent.innerHTML = html;
1018             root = parent.firstChild;
1019         }
1020         else
1021         {
1022             if (!parent || parent.nodeType != 9)
1023                 parent = document;
1024 
1025             if (!womb || womb.ownerDocument != parent)
1026                 womb = parent.createElement("div");
1027             womb.innerHTML = html;
1028 
1029             root = womb.firstChild;
1030             //womb.removeChild(root);
1031         }
1032 
1033         var domArgs = [root, this.tag.context, 0];
1034         domArgs.push.apply(domArgs, this.tag.domArgs);
1035         domArgs.push.apply(domArgs, outputs);
1036         this.tag.renderDOM.apply(self ? self : this.tag.subject, domArgs);
1037 
1038         return root;
1039     },
1040 
1041     append: function(args, parent, self)
1042     {
1043         if (!args)
1044             args = {};
1045 
1046         this.tag.compile();
1047 
1048         var outputs = [];
1049         var html = this.renderHTML(args, outputs, self);
1050         if (FBTrace.DBG_DOMPLATE)
1051             FBTrace.sysout("domplate.append html: "+html+"\n");
1052 
1053         if (!womb || womb.ownerDocument != parent.ownerDocument)
1054             womb = parent.ownerDocument.createElement("div");
1055         womb.innerHTML = html;
1056 
1057         var root = womb.firstChild;
1058         while (womb.firstChild)
1059             parent.appendChild(womb.firstChild);
1060 
1061         var domArgs = [root, this.tag.context, 0];
1062         domArgs.push.apply(domArgs, this.tag.domArgs);
1063         domArgs.push.apply(domArgs, outputs);
1064 
1065         if (FBTrace.DBG_DOMPLATE)
1066             FBTrace.sysout("domplate.append domArgs:", domArgs);
1067 
1068         this.tag.renderDOM.apply(self ? self : this.tag.subject, domArgs);
1069 
1070         return root;
1071     }
1072 };
1073 
1074 // ************************************************************************************************
1075 
1076 function defineTags()
1077 {
1078     for (var i = 0; i < arguments.length; ++i)
1079     {
1080         var tagName = arguments[i];
1081         var fn = new Function("var newTag = new DomplateTag('"+tagName+"'); return newTag.merge(arguments);");
1082 
1083         var fnName = tagName.toUpperCase();
1084         FBL[fnName] = fn;
1085     }
1086 }
1087 
1088 defineTags(
1089     "a", "button", "br", "canvas", "col", "colgroup", "div", "fieldset", "form", "h1", "h2", "h3", "hr",
1090      "img", "input", "label", "legend", "li", "ol", "optgroup", "option", "p", "pre", "select", "b",
1091     "span", "strong", "table", "tbody", "td", "textarea", "tfoot", "th", "thead", "tr", "tt", "ul",
1092     "iframe", "code"
1093 );
1094 
1095 })();
1096