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