1/*! JsRender v1.0.0-beta: http://github.com/BorisMoore/jsrender and http://jsviews.com/jsviews 2informal pre V1.0 commit counter: 51 */ 3/* 4 * Optimized version of jQuery Templates, for rendering to string. 5 * Does not require jQuery, or HTML DOM 6 * Integrates with JsViews (http://jsviews.com/jsviews) 7 * 8 * Copyright 2013, Boris Moore 9 * Released under the MIT License. 10 */ 11 12(function(global, jQuery, undefined) { 13 // global is the this object, which is window when running in the usual browser environment. 14 "use strict"; 15 16 if (jQuery && jQuery.views || global.jsviews) { return; } // JsRender is already loaded 17 18 //========================== Top-level vars ========================== 19 20 var versionNumber = "v1.0.0-beta", 21 22 $, jsvStoreName, rTag, rTmplString, indexStr, // nodeJsModule, 23 24//TODO tmplFnsCache = {}, 25 delimOpenChar0 = "{", delimOpenChar1 = "{", delimCloseChar0 = "}", delimCloseChar1 = "}", linkChar = "^", 26 27 rPath = /^(!*?)(?:null|true|false|\d[\d.]*|([\w$]+|\.|~([\w$]+)|#(view|([\w$]+))?)([\w$.^]*?)(?:[.[^]([\w$]+)\]?)?)$/g, 28 // none object helper view viewProperty pathTokens leafToken 29 30 rParams = /(\()(?=\s*\()|(?:([([])\s*)?(?:(\^?)(!*?[#~]?[\w$.^]+)?\s*((\+\+|--)|\+|-|&&|\|\||===|!==|==|!=|<=|>=|[<>%*:?\/]|(=))\s*|(!*?[#~]?[\w$.^]+)([([])?)|(,\s*)|(\(?)\\?(?:(')|("))|(?:\s*(([)\]])(?=\s*\.|\s*\^|\s*$)|[)\]])([([]?))|(\s+)/g, 31 // lftPrn0 lftPrn bound path operator err eq path2 prn comma lftPrn2 apos quot rtPrn rtPrnDot prn2 space 32 // (left paren? followed by (path? followed by operator) or (path followed by left paren?)) or comma or apos or quot or right paren or space 33 34 rNewLine = /[ \t]*(\r\n|\n|\r)/g, 35 rUnescapeQuotes = /\\(['"])/g, 36 rEscapeQuotes = /['"\\]/g, // Escape quotes and \ character 37 rBuildHash = /\x08(~)?([^\x08]+)\x08/g, 38 rTestElseIf = /^if\s/, 39 rFirstElem = /<(\w+)[>\s]/, 40 rAttrEncode = /[\x00`><"'&]/g, // Includes > encoding since rConvertMarkers in JsViews does not skip > characters in attribute strings 41 rHtmlEncode = rAttrEncode, 42 autoTmplName = 0, 43 viewId = 0, 44 charEntities = { 45 "&": "&", 46 "<": "<", 47 ">": ">", 48 "\x00": "�", 49 "'": "'", 50 '"': """, 51 "`": "`" 52 }, 53 htmlStr = "html", 54 tmplAttr = "data-jsv-tmpl", 55 $render = {}, 56 jsvStores = { 57 template: { 58 compile: compileTmpl 59 }, 60 tag: { 61 compile: compileTag 62 }, 63 helper: {}, 64 converter: {} 65 }, 66 67 // jsviews object ($.views if jQuery is loaded) 68 $views = { 69 jsviews: versionNumber, 70 render: $render, 71 settings: function(settings) { 72 $extend($viewsSettings, settings); 73 dbgMode($viewsSettings._dbgMode); 74 if ($viewsSettings.jsv) { 75 $viewsSettings.jsv(); 76 } 77 }, 78 sub: { 79 // subscription, e.g. JsViews integration 80 View: View, 81 Error: JsViewsError, 82 tmplFn: tmplFn, 83 parse: parseParams, 84 extend: $extend, 85 error: error, 86 syntaxError: syntaxError, 87 DataMap: DataMap 88 }, 89 _cnvt: convertVal, 90 _tag: renderTag, 91 92 _err: function(e) { 93 // Place a breakpoint here to intercept template rendering errors 94 return $viewsSettings._dbgMode ? ("Error: " + (e.message || e)) + ". " : ''; 95 } 96 }; 97 98 function retVal(val) { 99 return val; 100 } 101 102 function dbgBreak(val) { 103 debugger; 104 return val; 105 } 106 107 function dbgMode(debugMode) { 108 $viewsSettings._dbgMode = debugMode; 109 indexStr = debugMode ? "Error: #index in nested view: use #getIndex()" : ""; // If in debug mode set #index to a warning when in nested contexts 110 $tags("dbg", $helpers.dbg = $converters.dbg = debugMode ? dbgBreak : retVal); // If in debug mode, register {{dbg/}}, {{dbg:...}} and ~dbg() to insert break points for debugging. 111 } 112 113 function DataMap(getTarget) { 114 return { 115 getTgt: getTarget, 116 map: function(source) { 117 var theMap = this; // Instance of DataMap 118 if (theMap.src !== source) { 119 if (theMap.src) { 120 theMap.unmap(); 121 } 122 if (typeof source === "object") { 123 var changing, 124 target = getTarget.apply(theMap, arguments); 125 theMap.src = source; 126 theMap.tgt = target; 127 } 128 } 129 } 130 } 131 } 132 133 function JsViewsError(message, object) { 134 // Error exception type for JsViews/JsRender 135 // Override of $.views.sub.Error is possible 136 if (object && object.onError) { 137 if (object.onError(message) === false) { 138 return; 139 } 140 } 141 this.name = "JsRender Error"; 142 this.message = message || "JsRender error"; 143 } 144 145 function $extend(target, source) { 146 var name; 147 target = target || {}; 148 for (name in source) { 149 target[name] = source[name]; 150 } 151 return target; 152 } 153 154 (JsViewsError.prototype = new Error()).constructor = JsViewsError; 155 156 //========================== Top-level functions ========================== 157 158 //=================== 159 // jsviews.delimiters 160 //=================== 161 function $viewsDelimiters(openChars, closeChars, link) { 162 // Set the tag opening and closing delimiters and 'link' character. Default is "{{", "}}" and "^"
163 // openChars, closeChars: opening and closing strings, each with two characters 164 165 if (!$viewsSub.rTag || openChars) { 166 delimOpenChar0 = openChars ? openChars.charAt(0) : delimOpenChar0; // Escape the characters - since they could be regex special characters 167 delimOpenChar1 = openChars ? openChars.charAt(1) : delimOpenChar1; 168 delimCloseChar0 = closeChars ? closeChars.charAt(0) : delimCloseChar0; 169 delimCloseChar1 = closeChars ? closeChars.charAt(1) : delimCloseChar1; 170 linkChar = link || linkChar; 171 openChars = "\\" + delimOpenChar0 + "(\\" + linkChar + ")?\\" + delimOpenChar1; // Default is "{^{" 172 closeChars = "\\" + delimCloseChar0 + "\\" + delimCloseChar1; // Default is "}}" 173 // Build regex with new delimiters 174 // tag (followed by / space or }) or cvtr+colon or html or code 175 rTag = "(?:(?:(\\w+(?=[\\/\\s\\" + delimCloseChar0 + "]))|(?:(\\w+)?(:)|(>)|!--((?:[^-]|-(?!-))*)--|(\\*)))" 176 + "\\s*((?:[^\\" + delimCloseChar0 + "]|\\" + delimCloseChar0 + "(?!\\" + delimCloseChar1 + "))*?)"; 177 178 // make rTag available to JsViews (or other components) for parsing binding expressions 179 $viewsSub.rTag = rTag + ")"; 180 181 rTag = new RegExp(openChars + rTag + "(\\/)?|(?:\\/(\\w+)))" + closeChars, "g"); 182 183 // Default: bind tag converter colon html comment code params slash closeBlock 184 // /{(\^)?{(?:(?:(\w+(?=[\/\s}]))|(?:(\w+)?(:)|(>)|!--((?:[^-]|-(?!-))*)--|(\*)))\s*((?:[^}]|}(?!}))*?)(\/)?|(?:\/(\w+)))}}/g 185 186 rTmplString = new RegExp("<.*>|([^\\\\]|^)[{}]|" + openChars + ".*" + closeChars); 187 // rTmplString looks for html tags or { or } char not preceded by \\, or JsRender tags {{xxx}}. Each of these strings are considered 188 // NOT to be jQuery selectors 189 } 190 return [delimOpenChar0, delimOpenChar1, delimCloseChar0, delimCloseChar1, linkChar]; 191 } 192 193 //========= 194 // View.get 195 //========= 196 197 function getView(inner, type) { //view.get(inner, type) 198 if (!type) { 199 // view.get(type) 200 type = inner; 201 inner = undefined; 202 } 203 204 var views, i, l, found, 205 view = this, 206 root = !type || type === "root"; 207 // If type is undefined, returns root view (view under top view). 208 209 if (inner) { 210 // Go through views - this one, and all nested ones, depth-first - and return first one with given type. 211 found = view.type === type ? view : undefined; 212 if (!found) { 213 views = view.views; 214 if (view._.useKey) { 215 for (i in views) { 216 if (found = views[i].get(inner, type)) { 217 break; 218 } 219 } 220 } else for (i = 0, l = views.length; !found && i < l; i++) { 221 found = views[i].get(inner, type); 222 } 223 } 224 } else if (root) { 225 // Find root view. (view whose parent is top view) 226 while (view.parent.parent) { 227 found = view = view.parent; 228 } 229 } else while (view && !found) { 230 // Go through views - this one, and all parent ones - and return first one with given type. 231 found = view.type === type ? view : undefined; 232 view = view.parent; 233 } 234 return found; 235 } 236 237 function getNestedIndex() { 238 var view = this.get("item"); 239 return view ? view.index : undefined; 240 } 241 242 getNestedIndex.depends = function() { 243 return [this.get("item"), "index"]; 244 }; 245 246 function getIndex() { 247 return this.index; 248 } 249 250 getIndex.depends = function() { 251 return ["index"]; 252 }; 253 254 //========== 255 // View.hlp 256 //========== 257 258 function getHelper(helper) { 259 // Helper method called as view.hlp(key) from compiled template, for helper functions or template parameters ~foo 260 var wrapped, 261 view = this, 262 ctx = view.linkCtx, 263 res = (view.ctx || {})[helper]; 264 265 if (res === undefined && ctx && ctx.ctx) { 266 res = ctx.ctx[helper]; 267 } 268 if (res === undefined) { 269 res = $helpers[helper]; 270 } 271 272 if (res) { 273 if (typeof res === "function" && !res._wrp) { 274 wrapped = function() { 275 // If it is of type function, and not already wrapped, we will wrap it, so if called with no this pointer it will be called with the 276 // view as 'this' context. If the helper ~foo() was in a data-link expression, the view will have a 'temporary' linkCtx property too. 277 // Note that helper functions on deeper paths will have specific this pointers, from the preceding path. 278 // For example, ~util.foo() will have the ~util object as 'this' pointer 279 return res.apply((!this || this === global) ? view : this, arguments); 280 }; 281 wrapped._wrp = 1; 282 $extend(wrapped, res); // Attach same expandos (if any) to the wrapped function 283 } 284 } 285 return wrapped || res; 286 } 287 288 //============== 289 // jsviews._cnvt 290 //============== 291 292 function convertVal(converter, view, tagCtx) { 293 // self is template object or linkCtx object 294 var tmplConverter, tag, value, 295 boundTagCtx = +tagCtx === tagCtx && tagCtx, // if tagCtx is an integer, then it is the key for the boundTagCtx (compiled function to return the tagCtx) 296 linkCtx = view.linkCtx; // For data-link="{cvt:...}"... 297 298 if (boundTagCtx) { 299 // This is a bound tag: {^{xx:yyy}}. Call compiled function which returns the tagCtxs for current data 300 tagCtx = (boundTagCtx = view.tmpl.bnds[boundTagCtx-1])(view.data, view, $views); 301 } 302 303 value = tagCtx.args[0]; 304 if (converter || boundTagCtx) { 305 tag = linkCtx && linkCtx.tag || { 306 _: { 307 inline: !linkCtx, 308 bnd: boundTagCtx 309 }, 310 tagName: converter + ":", 311 flow: true, 312 _is: "tag" 313 }; 314 315 if (linkCtx) { 316 linkCtx.tag = tag; 317 tag.linkCtx = tag.linkCtx || linkCtx; 318 tagCtx.ctx = extendCtx(tagCtx.ctx, linkCtx.view.ctx); 319 } 320 tag.tagCtx = tagCtx; 321 tagCtx.view = view; 322 323 tag.ctx = tagCtx.ctx || {}; 324 delete tagCtx.ctx; 325 // Provide this tag on view, for addBindingMarkers on bound tags to add the tag to view._.bnds, associated with the tag id, 326 view._.tag = tag; 327 328 converter = converter !== "true" && converter; // If there is a convertBack but no convert, converter will be "true" 329 330 if (converter && ((tmplConverter = view.getRsc("converters", converter)) || error("Unknown converter: {{"+ converter + ":"))) { 331 // A call to {{cnvt: ... }} or {^{cnvt: ... }} or data-link="{cnvt: ... }" 332 tag.depends = tmplConverter.depends; 333 value = tmplConverter.apply(tag, tagCtx.args); 334 } 335 // Call onRender (used by JsViews if present, to add binding annotations around rendered content) 336 value = value != undefined ? value : ""; 337 value = boundTagCtx && view._.onRender 338 ? view._.onRender(value, view, boundTagCtx) 339 : value; 340 view._.tag = undefined; 341 } 342 return value; 343 } 344 345 //============= 346 // jsviews._tag 347 //============= 348 349 function getResource(resourceType, itemName) { 350 var res, store, 351 view = this; 352 while ((res === undefined) && view) { 353 store = view.tmpl[resourceType]; 354 res = store && store[itemName]; 355 view = view.parent; 356 } 357 return res || $views[resourceType][itemName]; 358 } 359 360 function renderTag(tagName, parentView, tmpl, tagCtxs, isRefresh) { 361 // Called from within compiled template function, to render a template tag 362 // Returns the rendered tag 363 364 var render, tag, tags, attr, parentTag, i, l, itemRet, tagCtx, tagCtxCtx, content, boundTagFn, tagDef, callInit, map, thisMap, args, 365 ret = "", 366 boundTagKey = +tagCtxs === tagCtxs && tagCtxs, // if tagCtxs is an integer, then it is the boundTagKey 367 linkCtx = parentView.linkCtx || 0, 368 ctx = parentView.ctx, 369 parentTmpl = tmpl || parentView.tmpl, 370 parentView_ = parentView._; 371 372 if (tagName._is === "tag") { 373 tag = tagName; 374 tagName = tag.tagName; 375 } 376 377 // Provide tagCtx, linkCtx and ctx access from tag 378 if (boundTagKey) { 379 // if tagCtxs is an integer, we are data binding 380 // Call compiled function which returns the tagCtxs for current data 381 tagCtxs = (boundTagFn = parentTmpl.bnds[boundTagKey-1])(parentView.data, parentView, $views); 382 } 383 384 l = tagCtxs.length; 385 tag = tag || linkCtx.tag; 386 for (i = 0; i < l; i++) { 387 tagCtx = tagCtxs[i]; 388 389 // Set the tmpl property to the content of the block tag, unless set as an override property on the tag 390 content = tagCtx.tmpl; 391 content = tagCtx.content = content && parentTmpl.tmpls[content - 1]; 392 tmpl = tagCtx.props.tmpl; 393 if (!i && (!tmpl || !tag)) { 394 tagDef = parentView.getRsc("tags", tagName) || error("Unknown tag: {{"+ tagName + "}}"); 395 } 396 tmpl = tmpl || (tag ? tag : tagDef).template || content; 397 tmpl = "" + tmpl === tmpl // if a string 398 ? parentView.getRsc("templates", tmpl) || $templates(tmpl) 399 : tmpl; 400 401 $extend(tagCtx, { 402 tmpl: tmpl, 403 render: renderContent, 404 index: i, 405 view: parentView, 406 ctx: extendCtx(tagCtx.ctx, ctx) // Extend parentView.ctx 407 }); // Extend parentView.ctx 408 409 if (!tag) { 410 // This will only be hit for initial tagCtx (not for {{else}}) - if the tag instance does not exist yet 411 // Instantiate tag if it does not yet exist 412 if (tagDef._ctr) { 413 // If the tag has not already been instantiated, we will create a new instance. 414 // ~tag will access the tag, even within the rendering of the template content of this tag. 415 // From child/descendant tags, can access using ~tag.parent, or ~parentTags.tagName 416// TODO provide error handling owned by the tag - using tag.onError 417// try { 418 tag = new tagDef._ctr(); 419 callInit = !!tag.init; 420// } 421// catch(e) { 422// tagDef.onError(e); 423// } 424 // Set attr on linkCtx to ensure outputting to the correct target attribute. 425 tag.attr = tag.attr || tagDef.attr || undefined; 426 // Setting either linkCtx.attr or this.attr in the init() allows per-instance choice of target attrib. 427 } else { 428 // This is a simple tag declared as a function, or with init set to false. We won't instantiate a specific tag constructor - just a standard instance object. 429 tag = { 430 // tag instance object if no init constructor 431 render: tagDef.render
432 }; 433 } 434 tag._ = { 435 inline: !linkCtx 436 }; 437 if (linkCtx) { 438 // Set attr on linkCtx to ensure outputting to the correct target attribute. 439 linkCtx.attr = tag.attr = linkCtx.attr || tag.attr; 440 linkCtx.tag = tag; 441 tag.linkCtx = linkCtx; 442 } 443 if (tag._.bnd = boundTagFn || linkCtx.fn) { 444 // Bound if {^{tag...}} or data-link="{tag...}" 445 tag._.arrVws = {}; 446 } else if (tag.dataBoundOnly) { 447 error("{^{" + tagName + "}} tag must be data-bound"); 448 } 449 tag.tagName = tagName; 450 tag.parent = parentTag = ctx && ctx.tag; 451 tag._is = "tag"; 452 tag._def = tagDef; 453 //TODO better perf for childTags() - keep child tag.tags array, (and remove child, when disposed) 454 // tag.tags = []; 455 // Provide this tag on view, for addBindingMarkers on bound tags to add the tag to view._.bnds, associated with the tag id, 456 } 457 tagCtx.tag = tag; 458 if (tag.map && tag.tagCtxs) { 459 tagCtx.map = tag.tagCtxs[i].map; // Copy over the compiled map instance from the previous tagCtxs to the refreshed ones 460 } 461 if (!tag.flow) { 462 tagCtxCtx = tagCtx.ctx = tagCtx.ctx || {}; 463 464 // tags hash: tag.ctx.tags, merged with parentView.ctx.tags, 465 tags = tag.parents = tagCtxCtx.parentTags = ctx && extendCtx(tagCtxCtx.parentTags, ctx.parentTags) || {}; 466 if (parentTag) { 467 tags[parentTag.tagName] = parentTag; 468 //TODO better perf for childTags: parentTag.tags.push(tag); 469 } 470 tagCtxCtx.tag = tag; 471 } 472 } 473 tag.tagCtxs = tagCtxs; 474 parentView_.tag = tag; 475 tag.rendering = {}; // Provide object for state during render calls to tag and elses. (Used by {{if}} and {{for}}...) 476 for (i = 0; i < l; i++) { 477 tagCtx = tag.tagCtx = tagCtxs[i]; 478 args = tagCtx.args; 479 if ((map = tagCtx.props.map || tag).map) { 480 if (args.length || !i || tagCtx.props.map) { 481 if (!args.length) { 482 args = [parentView.data]; // If opening tag or else with map property have no arg[0] specified, default the datamap source to the current data item. 483 } 484 thisMap = tagCtx.map = $extend(tagCtx.map || { unmap: map.unmap }, tagCtx.props); // Compiled map instance 485 if (thisMap.src !== args[0]) { 486 if (thisMap.src) { 487 thisMap.unmap(); 488 } 489 map.map.apply(thisMap, args); 490 } 491 args = [thisMap.tgt]; 492 } 493 } 494 tag.ctx = tagCtx.ctx; 495 496 if (!i && callInit) { 497 tag.init(tagCtx, linkCtx, tag.ctx); 498 tagCtx = tag.tagCtx; 499 callInit = undefined; 500 } 501 502 itemRet = undefined; 503 render = tag.render; 504 if (render = tag.render) { 505 itemRet = render.apply(tag, args); 506 } 507 itemRet = itemRet !== undefined 508 ? itemRet // Return result of render function unless it is undefined, in which case return rendered template 509 : tagCtx.tmpl && tagCtx.render(args[0], undefined, true) || (isRefresh ? undefined : ""); 510 // No return value from render, and no template/content tagCtx.render(), so return undefined 511 ret = ret ? ret + (itemRet || "") : itemRet; // If no rendered content, this will be undefined 512 } 513 514 delete tag.rendering; 515 516 tag.tagCtx = tag.tagCtxs[0]; 517 tag.ctx= tag.tagCtx.ctx; 518 519 if (tag._.inline && (attr = tag.attr) && attr !== htmlStr) { 520 // inline tag with attr set to "text" will insert HTML-encoded content - as if it was element-based innerText 521 ret = attr === "text" 522 ? $converters.html(ret) 523 : ""; 524 } 525 return boundTagKey && parentView._.onRender 526 // Call onRender (used by JsViews if present, to add binding annotations around rendered content) 527 ? parentView._.onRender(ret, parentView, boundTagKey) 528 : ret; 529 } 530 531 //================= 532 // View constructor 533 //================= 534 535 function View(context, type, parentView, data, template, key, contentTmpl, onRender) { 536 // Constructor for view object in view hierarchy. (Augmented by JsViews if JsViews is loaded) 537 var views, parentView_, tag, 538 isArray = type === "array", 539 self_ = { 540 key: 0, 541 useKey: isArray ? 0 : 1, 542 id: "" + viewId++, 543 onRender: onRender, 544 bnds: {} 545 }, 546 self = { 547 data: data, 548 tmpl: template, 549 content: contentTmpl, 550 views: isArray ? [] : {}, 551 parent: parentView, 552 type: type, 553 // If the data is an array, this is an 'array view' with a views array for each child 'item view' 554 // If the data is not an array, this is an 'item view' with a views 'map' object for any child nested views 555 // ._.useKey is non zero if is not an 'array view' (owning a data array). Uuse this as next key for adding to child views map 556 get: getView, 557 getIndex: getIndex, 558 getRsc: getResource, 559 hlp: getHelper, 560 _: self_, 561 _is: "view" 562 }; 563 if (parentView) { 564 views = parentView.views; 565 parentView_ = parentView._; 566 if (parentView_.useKey) { 567 // Parent is an 'item view'. Add this view to its views object 568 // self._key = is the key in the parent view map 569 views[self_.key = "_" + parentView_.useKey++] = self; 570 self.index = indexStr; 571 self.getIndex = getNestedIndex; 572 tag = parentView_.tag; 573 self_.bnd = isArray && (!tag || !!tag._.bnd && tag); // For array views that are data bound for collection change events, set the 574 // view._.bnd property to true for top-level link() or data-link="{for}", or to the tag instance for a data-bound tag, e.g. {^{for ...}} 575 } else { 576 // Parent is an 'array view'. Add this view to its views array 577 views.splice( 578 // self._.key = self.index - the index in the parent view array 579 self_.key = self.index = key, 580 0, self); 581 } 582 // If no context was passed in, use parent context 583 // If context was passed in, it should have been merged already with parent context 584 self.ctx = context || parentView.ctx; 585 } else { 586 self.ctx = context; 587 } 588 return self; 589 } 590 591 //============= 592 // Registration 593 //============= 594 595 function compileChildResources(parentTmpl) { 596 var storeName, resources, resourceName, settings, compile; 597 for (storeName in jsvStores) { 598 settings = jsvStores[storeName]; 599 if ((compile = settings.compile) && (resources = parentTmpl[storeName + "s"])) { 600 for (resourceName in resources) { 601 // compile child resource declarations (templates, tags, converters or helpers) 602 resources[resourceName] = compile(resourceName, resources[resourceName], parentTmpl, storeName, settings); 603 } 604 } 605 } 606 } 607 608 function compileTag(name, tagDef, parentTmpl) { 609 var init, tmpl; 610 if (typeof tagDef === "function") { 611 // Simple tag declared as function. No presenter instantation. 612 tagDef = { 613 depends: tagDef.depends, 614 render: tagDef 615 }; 616 } else { 617 // Tag declared as object, used as the prototype for tag instantiation (control/presenter) 618 if (tmpl = tagDef.template) { 619 tagDef.template = "" + tmpl === tmpl ? ($templates[tmpl] || $templates(tmpl)) : tmpl; 620 } 621 if (tagDef.init !== false) { 622 // Set int: false on tagDef if you want to provide just a render method, or render and template, but no constuctor or prototype. 623 // so equivalent to setting tag to render function, except you can also provide a template. 624 init = tagDef._ctr = function(tagCtx) {}; 625 (init.prototype = tagDef).constructor = init; 626 } 627 } 628 if (parentTmpl) { 629 tagDef._parentTmpl = parentTmpl; 630 } 631//TODO tagDef.onError = function(e) { 632// var error; 633// if (error = this.prototype.onError) { 634// error.call(this, e); 635// } else { 636// throw e; 637// } 638// } 639 return tagDef; 640 } 641 642 function compileTmpl(name, tmpl, parentTmpl, storeName, storeSettings, options) { 643 // tmpl is either a template object, a selector for a template script block, the name of a compiled template, or a template object 644 645 //==== nested functions ==== 646 function tmplOrMarkupFromStr(value) { 647 // If value is of type string - treat as selector, or name of compiled template 648 // Return the template object, if already compiled, or the markup string 649 650 if (("" + value === value) || value.nodeType > 0) { 651 try { 652 elem = value.nodeType > 0 653 ? value 654 : !rTmplString.test(value) 655 // If value is a string and does not contain HTML or tag content, then test as selector 656 && jQuery && jQuery(global.document).find(value)[0]; 657 // If selector is valid and returns at least one element, get first element 658 // If invalid, jQuery will throw. We will stay with the original string. 659 } catch (e) {} 660 661 if (elem) { 662 // Generally this is a script element. 663 // However we allow it to be any element, so you can for example take the content of a div, 664 // use it as a template, and replace it by the same content rendered against data. 665 // e.g. for linking the content of a div to a container, and using the initial content as template: 666 // $.link("#content", model, {tmpl: "#content"}); 667 668 value = elem.getAttribute(tmplAttr); 669 name = name || value; 670 value = $templates[value]; 671 if (!value) { 672 // Not already compiled and cached, so compile and cache the name 673 // Create a name for compiled template if none provided 674 name = name || "_" + autoTmplName++; 675 elem.setAttribute(tmplAttr, name); 676 // Use tmpl as options 677 value = $templates[name] = compileTmpl(name, elem.innerHTML, parentTmpl, storeName, storeSettings, options); 678 } 679 } 680 return value; 681 } 682 // If value is not a string, return undefined 683 } 684 685 var tmplOrMarkup, elem; 686 687 //==== Compile the template ==== 688 tmpl = tmpl || ""; 689 tmplOrMarkup = tmplOrMarkupFromStr(tmpl); 690 691 // If options, then this was already compiled from a (script) element template declaration. 692 // If not, then if tmpl is a template object, use it for options 693 options = options || (tmpl.markup ? tmpl : {}); 694 options.tmplName = name; 695 if (parentTmpl) { 696 options._parentTmpl = parentTmpl; 697 } 698 // If tmpl is not a markup string or a selector string, then it must be a template object 699 // In that case, get it from the markup property of the object 700 if (!tmplOrMarkup && tmpl.markup && (tmplOrMarkup = tmplOrMarkupFromStr(tmpl.markup))) { 701 if (tmplOrMarkup.fn && (tmplOrMarkup.debug !== tmpl.debug || tmplOrMarkup.allowCode !== tmpl.allowCode)) { 702 // if the string references a compiled template object, but the debug or allowCode props are different, need to recompile 703 tmplOrMarkup = tmplOrMarkup.markup; 704 } 705 } 706 if (tmplOrMarkup !== undefined) { 707 if (name && !parentTmpl) { 708 $render[name] = function() { 709 return tmpl.render.apply(tmpl, arguments); 710 }; 711 } 712 if (tmplOrMarkup.fn || tmpl.fn) { 713 // tmpl is already compiled, so use it, or if different name is provided, clone it 714 if (tmplOrMarkup.fn) { 715 if (name && name !== tmplOrMarkup.tmplName) { 716 tmpl = extendCtx(options, tmplOrMarkup); 717 } else { 718 tmpl = tmplOrMarkup; 719 } 720 } 721 } else { 722 // tmplOrMarkup is a markup string, not a compiled template 723 // Create template object 724 tmpl = TmplObject(tmplOrMarkup, options); 725 // Compile to AST and then to compiled function 726 tmplFn(tmplOrMarkup.replace(rEscapeQuotes, "\\$&"), tmpl); 727 } 728 compileChildResources(options); 729 return tmpl; 730 } 731 } 732 //==== /end of function compile ==== 733 734 function TmplObject(markup, options) {
735 // Template object constructor 736 var htmlTag, 737 wrapMap = $viewsSettings.wrapMap || {}, 738 tmpl = $extend( 739 { 740 markup: markup, 741 tmpls: [], 742 links: {}, // Compiled functions for link expressions 743 tags: {}, // Compiled functions for bound tag expressions 744 bnds: [], 745 _is: "template", 746 render: renderContent 747 }, 748 options 749 ); 750 751 if (!options.htmlTag) { 752 // Set tmpl.tag to the top-level HTML tag used in the template, if any... 753 htmlTag = rFirstElem.exec(markup); 754 tmpl.htmlTag = htmlTag ? htmlTag[1].toLowerCase() : ""; 755 } 756 htmlTag = wrapMap[tmpl.htmlTag]; 757 if (htmlTag && htmlTag !== wrapMap.div) { 758 // When using JsViews, we trim templates which are inserted into HTML contexts where text nodes are not rendered (i.e. not 'Phrasing Content'). 759 // Currently not trimmed for <li> tag. (Not worth adding perf cost) 760 tmpl.markup = $.trim(tmpl.markup); 761 } 762 763 return tmpl; 764 } 765 766 function registerStore(storeName, storeSettings) { 767 768 function theStore(name, item, parentTmpl) { 769 // The store is also the function used to add items to the store. e.g. $.templates, or $.views.tags 770 771 // For store of name 'thing', Call as: 772 // $.views.things(items[, parentTmpl]), 773 // or $.views.things(name, item[, parentTmpl]) 774 775 var onStore, compile, itemName, thisStore; 776 777 if (name && "" + name !== name && !name.nodeType && !name.markup) { 778 // Call to $.views.things(items[, parentTmpl]), 779 780 // Adding items to the store 781 // If name is a map, then item is parentTmpl. Iterate over map and call store for key. 782 for (itemName in name) { 783 theStore(itemName, name[itemName], item); 784 } 785 return $views; 786 } 787 // Adding a single unnamed item to the store 788 if (item === undefined) { 789 item = name; 790 name = undefined; 791 } 792 if (name && "" + name !== name) { // name must be a string 793 parentTmpl = item; 794 item = name; 795 name = undefined; 796 } 797 thisStore = parentTmpl ? parentTmpl[storeNames] = parentTmpl[storeNames] || {} : theStore; 798 compile = storeSettings.compile; 799 if (onStore = $viewsSub.onBeforeStoreItem) { 800 // e.g. provide an external compiler or preprocess the item. 801 compile = onStore(thisStore, name, item, compile) || compile; 802 } 803 if (!name) { 804 item = compile(undefined, item); 805 } else if (item === null) { 806 // If item is null, delete this entry 807 delete thisStore[name]; 808 } else { 809 thisStore[name] = compile ? (item = compile(name, item, parentTmpl, storeName, storeSettings)) : item; 810 } 811 if (compile && item) { 812 item._is = storeName;
812 // Only do this for compiled objects (tags, templates...) 813 } 814 if (onStore = $viewsSub.onStoreItem) { 815 // e.g. JsViews integration 816 onStore(thisStore, name, item, compile); 817 } 818 return item; 819 } 820 821 var storeNames = storeName + "s"; 822 823 $views[storeNames] = theStore; 824 jsvStores[storeName] = storeSettings; 825 } 826 827 //============== 828 // renderContent 829 //============== 830 831 function renderContent(data, context, noIteration, parentView, key, onRender) { 832 // Render template against data as a tree of subviews (nested rendered template instances), or as a string (top-level template). 833 // If the data is the parent view, treat as noIteration, re-render with the same data context. 834 var i, l, dataItem, newView, childView, itemResult, swapContent, tagCtx, contentTmpl, tag_, outerOnRender, tmplName, tmpl, 835 self = this, 836 allowDataLink = !self.attr || self.attr === htmlStr, 837 result = ""; 838 839 if (key === true) { 840 swapContent = true; 841 key = 0; 842 } 843 if (self.tag) { 844 // This is a call from renderTag or tagCtx.render() 845 tagCtx = self; 846 self = self.tag; 847 tag_ = self._; 848 tmplName = self.tagName; 849 tmpl = tagCtx.tmpl; 850 context = extendCtx(context, self.ctx); 851 contentTmpl = tagCtx.content; // The wrapped content - to be added to views, below 852 if (tagCtx.props.link === false) { 853 // link=false setting on block tag 854 // We will override inherited value of link by the explicit setting link=false taken from props 855 // The child views of an unlinked view are also unlinked. So setting child back to true will not have any effect. 856 context = context || {}; 857 context.link = false; 858 } 859 parentView = parentView || tagCtx.view; 860 data = data === undefined ? parentView : data; 861 } else { 862 tmpl = self.jquery && (self[0] || error('Unknown template: "' + self.selector + '"')) // This is a call from $(selector).render 863 || self; 864 } 865 if (tmpl) { 866 if (!parentView && data && data._is === "view") { 867 parentView = data; // When passing in a view to render or link (and not passing in a parent view) use the passed in view as parentView 868 } 869 if (parentView) { 870 contentTmpl = contentTmpl || parentView.content; // The wrapped content - to be added as #content property on views, below 871 onRender = onRender || parentView._.onRender; 872 if (data === parentView) { 873 // Inherit the data from the parent view. 874 // This may be the contents of an {{if}} block 875 // Set isLayout = true so we don't iterate the if block if the data is an array. 876 data = parentView.data; 877 noIteration = true; 878 } 879 context = extendCtx(context, parentView.ctx); 880 } 881 if (!parentView || parentView.data === undefined) { 882 (context = context || {}).root = data; // Provide ~root as shortcut to top-level data. 883 } 884 885 // Set additional context on views created here, (as modified context inherited from the parent, and to be inherited by child views) 886 // Note: If no jQuery, $extend does not support chained copies - so limit extend() to two parameters 887 888 if (!tmpl.fn) { 889 tmpl = $templates[tmpl] || $templates(tmpl); 890 } 891 892 if (tmpl) { 893 onRender = (context && context.link) !== false && allowDataLink && onRender; 894 // If link===false, do not call onRender, so no data-linking marker nodes 895 outerOnRender = onRender; 896 if (onRender === true) { 897 // Used by view.refresh(). Don't create a new wrapper view. 898 outerOnRender = undefined; 899 onRender = parentView._.onRender; 900 } 901 context = tmpl.helpers 902 ? extendCtx(tmpl.helpers, context) 903 : context; 904 if ($.isArray(data) && !noIteration) { 905 // Create a view for the array, whose child views correspond to each data item. (Note: if key and parentView are passed in 906 // along with parent view, treat as insert -e.g. from view.addViews - so parentView is already the view item for array) 907 newView = swapContent 908 ? parentView : 909 (key !== undefined && parentView) || View(context, "array", parentView, data, tmpl, key, contentTmpl, onRender); 910 for (i = 0, l = data.length; i < l; i++) { 911 // Create a view for each data item. 912 dataItem = data[i]; 913 childView = View(context, "item", newView, dataItem, tmpl, (key || 0) + i, contentTmpl, onRender); 914 itemResult = tmpl.fn(dataItem, childView, $views); 915 result += newView._.onRender ? newView._.onRender(itemResult, childView) : itemResult; 916 } 917 } else { 918 // Create a view for singleton data object. The type of the view will be the tag name, e.g. "if" or "myTag" except for 919 // "item", "array" and "data" views. A "data" view is from programatic render(object) against a 'singleton'. 920 newView = swapContent ? parentView : View(context, tmplName || "data", parentView, data, tmpl, key, contentTmpl, onRender); 921 if (tag_ && !self.flow) { 922 newView.tag = self; 923 } 924 result += tmpl.fn(data, newView, $views); 925 } 926 return outerOnRender ? outerOnRender(result, newView) : result; 927 } 928 } 929 return ""; 930 } 931 932 //=========================== 933 // Build and compile template 934 //=========================== 935 936 // Generate a reusable function that will serve to render a template against data 937 // (Compile AST then build template function) 938 939 function error(message) { 940 throw new $viewsSub.Error(message); 941 } 942 943 function syntaxError(message) { 944 error("Syntax error\n" + message); 945 } 946 947 function tmplFn(markup, tmpl, isLinkExpr, convertBack) { 948 // Compile markup to AST (abtract syntax tree) then build the template function code from the AST nodes
949 // Used for compiling templates, and also by JsViews to build functions for data link expressions 950 951 //==== nested functions ==== 952 function pushprecedingContent(shift) { 953 shift -= loc; 954 if (shift) { 955 content.push(markup.substr(loc, shift).replace(rNewLine, "\\n")); 956 } 957 } 958 959 function blockTagCheck(tagName) { 960 tagName && syntaxError('Unmatched or missing tag: "{{/' + tagName + '}}" in template:\n' + markup); 961 } 962 963 function parseTag(all, bind, tagName, converter, colon, html, comment, codeTag, params, slash, closeBlock, index) { 964 965 // bind tag converter colon html comment code params slash closeBlock 966 // /{(\^)?{(?:(?:(\w+(?=[\/\s}]))|(?:(\w+)?(:)|(>)|!--((?:[^-]|-(?!-))*)--|(\*)))\s*((?:[^}]|}(?!}))*?)(\/)?|(?:\/(\w+)))}}/g 967 // Build abstract syntax tree (AST): [tagName, converter, params, content, hash, bindings, contentMarkup] 968 if (html) { 969 colon = ":"; 970 converter = htmlStr; 971 } 972 slash = slash || isLinkExpr; 973 var noError, current0, 974 pathBindings = bind && [], 975 code = "", 976 hash = "", 977 passedCtx = "", 978 // Block tag if not self-closing and not {{:}} or {{>}} (special case) and not a data-link expression 979 block = !slash && !colon && !comment; 980 981 //==== nested helper function ==== 982 tagName = tagName || colon; 983 pushprecedingContent(index); 984 loc = index + all.length; // location marker - parsed up to here 985 if (codeTag) { 986 if (allowCode) { 987 content.push(["*", "\n" + params.replace(rUnescapeQuotes, "$1") + "\n"]); 988 } 989 } else if (tagName) { 990 if (tagName === "else") { 991 if (rTestElseIf.test(params)) { 992 syntaxError('for "{{else if expr}}" use "{{else expr}}"'); 993 } 994 pathBindings = current[6]; 995 current[7] = markup.substring(current[7], index); // contentMarkup for block tag 996 current = stack.pop(); 997 content = current[3]; 998 block = true; 999 } 1000 if (params) { 1001 // remove newlines from the params string, to avoid compiled code errors for unterminated strings 1002 params = params.replace(rNewLine, " "); 1003 code = parseParams(params, pathBindings, tmpl) 1004 .replace(rBuildHash, function(all, isCtx, keyValue) { 1005 if (isCtx) { 1006 passedCtx += keyValue + ","; 1007 } else { 1008 hash += keyValue + ","; 1009 } 1010 return ""; 1011 }); 1012 } 1013 hash = hash.slice(0, -1); 1014 code = code.slice(0, -1); 1015 noError = hash && (hash.indexOf("noerror:true") + 1) && hash || ""; 1016 1017 newNode = [ 1018 tagName, 1019 converter || !!convertBack || "", 1020 code, 1021 block && [], 1022 'params:"' + params + '",props:{' + hash + "}" 1023 + (passedCtx ? ",ctx:{" + passedCtx.slice(0, -1) + "}" : ""), 1024 noError, 1025 pathBindings || 0 1026 ]; 1027 content.push(newNode); 1028 if (block) { 1029 stack.push(current); 1030 current = newNode; 1031 current[7] = loc; // Store current location of open tag, to be able to add contentMarkup when we reach closing tag 1032 } 1033 } else if (closeBlock) { 1034 current0 = current[0]; 1035 blockTagCheck(closeBlock !== current0 && current0 !== "else" && closeBlock); 1036 current[7] = markup.substring(current[7], index); // contentMarkup for block tag 1037 current = stack.pop(); 1038 } 1039 blockTagCheck(!current && closeBlock); 1040 content = current[3]; 1041 } 1042 //==== /end of nested functions ==== 1043 1044 var newNode, 1045 allowCode = tmpl && tmpl.allowCode, 1046 astTop = [], 1047 loc = 0, 1048 stack = [], 1049 content = astTop, 1050 current = [, , , astTop]; 1051 1052//TODO result = tmplFnsCache[markup]; // Only cache if template is not named and markup length < ..., 1053//and there are no bindings or subtemplates?? Consider standard optimization for data-link="a.b.c" 1054// if (result) { 1055// tmpl.fn = result; 1056// } else { 1057 1058// result = markup; 1059 1060 blockTagCheck(stack[0] && stack[0][3].pop()[0]); 1061 // Build the AST (abstract syntax tree) under astTop 1062 markup.replace(rTag, parseTag); 1063 1064 pushprecedingContent(markup.length); 1065 1066 if (loc = astTop[astTop.length - 1]) { 1067 blockTagCheck("" + loc !== loc && (+loc[7] === loc[7]) && loc[0]); 1068 } 1069// result = tmplFnsCache[markup] = buildCode(astTop, tmpl); 1070// } 1071 return buildCode(astTop, isLinkExpr ? markup : tmpl, isLinkExpr); 1072 } 1073 1074 function buildCode(ast, tmpl, isLinkExpr) { 1075 // Build the template function code from the AST nodes, and set as property on the passed-in template object
1076 // Used for compiling templates, and also by JsViews to build functions for data link expressions 1077 var i, node, tagName, converter, params, hash, hasTag, hasEncoder, getsVal, hasCnvt, useCnvt, tmplBindings, pathBindings, 1078 nestedTmpls, tmplName, nestedTmpl, tagAndElses, content, markup, nextIsElse, oldCode, isElse, isGetVal, prm, tagCtxFn, 1079 tmplBindingKey = 0, 1080 code = "", 1081 noError = "", 1082 tmplOptions = {}, 1083 l = ast.length; 1084 1085 if ("" + tmpl === tmpl) { 1086 tmplName = isLinkExpr ? 'data-link="' + tmpl.replace(rNewLine, " ").slice(1, -1) + '"' : tmpl; 1087 tmpl = 0; 1088 } else { 1089 tmplName = tmpl.tmplName || "unnamed"; 1090 if (tmpl.allowCode) { 1091 tmplOptions.allowCode = true; 1092 } 1093 if (tmpl.debug) { 1094 tmplOptions.debug = true; 1095 } 1096 tmplBindings = tmpl.bnds; 1097 nestedTmpls = tmpl.tmpls; 1098 } 1099 for (i = 0; i < l; i++) { 1100 // AST nodes: [tagName, converter, params, content, hash, noError, pathBindings, contentMarkup, link] 1101 node = ast[i]; 1102 1103 // Add newline for each callout to t() c() etc. and each markup string 1104 if ("" + node === node) { 1105 // a markup string to be inserted 1106 code += '\nret+="' + node + '";'; 1107 } else { 1108 // a compiled tag expression to be inserted 1109 tagName = node[0]; 1110 if (tagName === "*") { 1111 // Code tag: {{* }} 1112 code += "" + node[1]; 1113 } else { 1114 converter = node[1]; 1115 params = node[2]; 1116 content = node[3]; 1117 hash = node[4]; 1118 noError = node[5]; 1119 markup = node[7]; 1120 1121 if (!(isElse = tagName === "else")) { 1122 tmplBindingKey = 0; 1123 if (tmplBindings && (pathBindings = node[6])) { // Array of paths, or false if not data-bound 1124 tmplBindingKey = tmplBindings.push(pathBindings); 1125 } 1126 } 1127 if (isGetVal = tagName === ":") { 1128 if (converter) { 1129 tagName = converter === htmlStr ? ">" : converter + tagName; 1130 } 1131 if (noError) { 1132 // If the tag includes noerror=true, we will do a try catch around expressions for named or unnamed parameters 1133 // passed to the tag, and return the empty string for each expression if it throws during evaluation 1134 //TODO This does not work for general case - supporting noError on multiple expressions, e.g. tag args and properties. 1135 //Consider replacing with try<a.b.c(p,q) + a.d, xxx> and return the value of the expression a.b.c(p,q) + a.d, or, if it throws, return xxx||'' (rather than always the empty string) 1136 prm = "prm" + i; 1137 noError = "try{var " + prm + "=[" + params + "][0];}catch(e){" + prm + '="";}\n'; 1138 params = prm; 1139 } 1140 } else { 1141 if (content) { 1142 // Create template object for nested template 1143 nestedTmpl = TmplObject(markup, tmplOptions); 1144 nestedTmpl.tmplName = tmplName + "/" + tagName; 1145 // Compile to AST and then to compiled function 1146 buildCode(content, nestedTmpl); 1147 nestedTmpls.push(nestedTmpl); 1148 } 1149 1150 if (!isElse) { 1151 // This is not an else tag. 1152 tagAndElses = tagName; 1153 // Switch to a new code string for this bound tag (and its elses, if it has any) - for returning the tagCtxs array 1154 oldCode = code; 1155 code = ""; 1156 } 1157 nextIsElse = ast[i + 1]; 1158 nextIsElse = nextIsElse && nextIsElse[0] === "else"; 1159 } 1160 1161 hash += ",args:[" + params + "]}"; 1162 1163 if (isGetVal && pathBindings || converter && tagName !== ">") { 1164 // For convertVal we need a compiled function to return the new tagCtx(s) 1165 tagCtxFn = new Function("data,view,j,u", " // " 1166 + tmplName + " " + tmplBindingKey + " " + tagName + "\n" + noError + "return {" + hash + ";"); 1167 tagCtxFn.paths = pathBindings; 1168 tagCtxFn._ctxs = tagName; 1169 if (isLinkExpr) { 1170 return tagCtxFn; 1171 } 1172 useCnvt = 1; 1173 } 1174 1175 code += (isGetVal 1176 ? "\n" + (pathBindings ? "" : noError) + (isLinkExpr ? "return " : "ret+=") + (useCnvt // Call _cnvt if there is a converter: {{cnvt: ... }} or {^{cnvt: ... }} 1177 ? (useCnvt = 0, hasCnvt = true, 'c("' + converter + '",view,' + (pathBindings 1178 ? ((tmplBindings[tmplBindingKey - 1] = tagCtxFn), tmplBindingKey) // Store the compiled tagCtxFn in tmpl.bnds, and pass the key to convertVal() 1179 : "{" + hash) + ");") 1180 : tagName === ">" 1181 ? (hasEncoder = true, "h(" + params + ");") 1182 : (getsVal = true, "(v=" + params + ")!=" + (isLinkExpr ? "=" : "") + 'u?v:"";') // Strict equality just for data-link="title{:expr}" so expr=null will remove title attribute 1183 ) 1184 : (hasTag = true, "{view:view,tmpl:" // Add this tagCtx to the compiled code for the tagCtxs to be passed to renderTag() 1185 + (content ? nestedTmpls.length: "0") + "," // For block tags, pass in the key (nestedTmpls.length) to the nested content template 1186 + hash + ",")); 1187 1188 if (tagAndElses && !nextIsElse) { 1189 code = "[" + code.slice(0, -1) + "]"; // This is a data-link expression or the last {{else}} of an inline bound tag. We complete the code for returning the tagCtxs array 1190 if (isLinkExpr || pathBindings) { 1191 // This is a bound tag (data-link expression or inline bound tag {^{tag ...}}) so we store a compiled tagCtxs function in tmp.bnds 1192 code = new Function("data,view,j,u", " // " + tmplName + " " + tmplBindingKey + " " + tagAndElses + "\nreturn " + code + ";"); 1193 if (pathBindings) { 1194 (tmplBindings[tmplBindingKey - 1] = code).paths = pathBindings;
1195 } 1196 code._ctxs = tagName; 1197 if (isLinkExpr) { 1198 return code; // For a data-link expression we return the compiled tagCtxs function 1199 } 1200 } 1201 1202 // This is the last {{else}} for an inline tag. 1203 // For a bound tag, pass the tagCtxs fn lookup key to renderTag. 1204 // For an unbound tag, include the code directly for evaluating tagCtxs array 1205 code = oldCode + '\nret+=t("' + tagAndElses + '",view,this,' + (tmplBindingKey || code) + ");"; 1206 pathBindings = 0; 1207 tagAndElses = 0; 1208 } 1209 } 1210 } 1211 } 1212 // Include only the var references that are needed in the code 1213 code = "// " + tmplName 1214 + "\nvar j=j||" + (jQuery ? "jQuery." : "js") + "views" 1215 + (getsVal ? ",v" : "") // gets value 1216 + (hasTag ? ",t=j._tag" : "") // has tag 1217 + (hasCnvt ? ",c=j._cnvt" : "") // converter 1218 + (hasEncoder ? ",h=j.converters.html" : "") // html converter 1219 + (isLinkExpr ? ";\n" : ',ret="";\n') 1220 + ($viewsSettings.tryCatch ? "try{\n" : "") 1221 + (tmplOptions.debug ? "debugger;" : "") 1222 + code + (isLinkExpr ? "\n" : "\nreturn ret;\n") 1223 + ($viewsSettings.tryCatch ? "\n}catch(e){return j._err(e);}" : ""); 1224 try { 1225 code = new Function("data,view,j,u", code); 1226 } catch (e) { 1227 syntaxError("Compiled template code:\n\n" + code, e); 1228 } 1229 if (tmpl) { 1230 tmpl.fn = code; 1231 } 1232 return code; 1233 } 1234 1235 function parseParams(params, bindings, tmpl) { 1236 1237 //function pushBindings() { // Consider structured path bindings 1238 // if (bindings) { 1239 // named ? bindings[named] = bindings.pop(): bindings.push(list = []); 1240 // } 1241 //} 1242 1243 function parseTokens(all, lftPrn0, lftPrn, bound, path, operator, err, eq, path2, prn, comma, lftPrn2, apos, quot, rtPrn, rtPrnDot, prn2, space, index, full) { 1244 //rParams = /(\()(?=\s*\()|(?:([([])\s*)?(?:(\^?)(!*?[#~]?[\w$.^]+)?\s*((\+\+|--)|\+|-|&&|\|\||===|!==|==|!=|<=|>=|[<>%*:?\/]|(=))\s*|(!*?[#~]?[\w$.^]+)([([])?)|(,\s*)|(\(?)\\?(?:(')|("))|(?:\s*(([)\]])(?=\s*\.|\s*\^)|[)\]])([([]?))|(\s+)/g, 1245 // lftPrn0 lftPrn bound path operator err eq path2 prn comma lftPrn2 apos quot rtPrn rtPrnDot prn2 space 1246 // (left paren? followed by (path? followed by operator) or (path followed by paren?)) or comma or apos or quot or right paren or space 1247 var expr; 1248 operator = operator || ""; 1249 lftPrn = lftPrn || lftPrn0 || lftPrn2; 1250 path = path || path2; 1251 prn = prn || prn2 || ""; 1252 1253 function parsePath(allPath, not, object, helper, view, viewProperty, pathTokens, leafToken) { 1254 // rPath = /^(?:null|true|false|\d[\d.]*|(!*?)([\w$]+|\.|~([\w$]+)|#(view|([\w$]+))?)([\w$.^]*?)(?:[.[^]([\w$]+)\]?)?)$/g, 1255 // none object helper view viewProperty pathTokens leafToken 1256 if (object) { 1257 if (bindings) { 1258 if (named === "linkTo") { 1259 bindto = bindings._jsvto = bindings._jsvto || []; 1260 bindto.push(path); 1261 } 1262 if (!named || boundName) { 1263 bindings.push(path.slice(not.length)); // Add path binding for paths on props and args, 1264// list.push(path); 1265 } 1266 } 1267 if (object !== ".") { 1268 var ret = (helper 1269 ? 'view.hlp("' + helper + '")' 1270 : view 1271 ? "view" 1272 : "data") 1273 + (leafToken 1274 ? (viewProperty 1275 ? "." + viewProperty 1276 : helper 1277 ? "" 1278 : (view ? "" : "." + object) 1279 ) + (pathTokens || "") 1280 : (leafToken = helper ? "" : view ? viewProperty || "" : object, "")); 1281 1282 ret = ret + (leafToken ? "." + leafToken : ""); 1283 1284 return not + (ret.slice(0, 9) === "view.data" 1285 ? ret.slice(5) // convert #view.data... to data... 1286 : ret); 1287 } 1288 } 1289 return allPath; 1290 } 1291 1292 if (err && !aposed && !quoted) { 1293 syntaxError(params); 1294 } else { 1295 if (bindings && rtPrnDot && !aposed && !quoted) { 1296 // This is a binding to a path in which an object is returned by a helper/data function/expression, e.g. foo()^x.y or (a?b:c)^x.y 1297 // We create a compiled function to get the object instance (which will be called when the dependent data of the subexpression changes, to return the new object, and trigger re-binding of the subsequent path) 1298 if (!named || boundName || bindto) { 1299 expr = pathStart[parenDepth]; 1300 if (full.length - 1 > index - expr) { // We need to compile a subexpression 1301 expr = full.slice(expr, index + 1); 1302 rtPrnDot = delimOpenChar1 + ":" + expr + delimCloseChar0; // The parameter or function subexpression 1303 //TODO Optimize along the lines of: 1304 //var paths = []; 1305 //rtPrnDot = tmplLinks[rtPrnDot] = tmplLinks[rtPrnDot] || tmplFn(delimOpenChar0 + rtPrnDot + delimCloseChar1, tmpl, true, paths); // Compile the expression (or use cached copy already in tmpl.links) 1306 //rtPrnDot.paths = rtPrnDot.paths || paths; 1307 1308 rtPrnDot = tmplLinks[rtPrnDot] = tmplLinks[rtPrnDot] || tmplFn(delimOpenChar0 + rtPrnDot + delimCloseChar1, tmpl, true); // Compile the expression (or use cached copy already in tmpl.links) 1309 if (!rtPrnDot.paths) { 1310 parseParams(expr, rtPrnDot.paths = [], tmpl); 1311 } 1312 (bindto || bindings).push({_jsvOb: rtPrnDot}); // Insert special object for in path bindings, to be used for binding the compiled sub expression () 1313 //list.push({_jsvOb: rtPrnDot}); 1314 } 1315 } 1316 } 1317 return (aposed 1318 // within single-quoted string
1319 ? (aposed = !apos, (aposed ? all : '"')) 1320 : quoted 1321 // within double-quoted string 1322 ? (quoted = !quot, (quoted ? all : '"')) 1323 : 1324 ( 1325 (lftPrn 1326 ? (parenDepth++, pathStart[parenDepth] = index++, lftPrn) 1327 : "") 1328 + (space 1329 ? (parenDepth 1330 ? "" 1331 //: (pushBindings(), named 1332 // : ",") 1333 : named 1334 ? (named = boundName = bindto = false, "\b") 1335 : "," 1336 ) 1337 : eq 1338 // named param 1339 // Insert backspace \b (\x08) as separator for named params, used subsequently by rBuildHash 1340 ? (parenDepth && syntaxError(params), named = path, boundName = bound, /*pushBindings(),*/ '\b' + path + ':') 1341 : path 1342 // path 1343 ? (path.split("^").join(".").replace(rPath, parsePath) 1344 + (prn 1345 ? (fnCall[++parenDepth] = true, path.charAt(0) !== "." && (pathStart[parenDepth] = index), prn) 1346 : operator) 1347 ) 1348 : operator 1349 ? operator 1350 : rtPrn 1351 // function 1352 ? ((fnCall[parenDepth--] = false, rtPrn) 1353 + (prn 1354 ? (fnCall[++parenDepth] = true, prn) 1355 : "") 1356 ) 1357 : comma 1358 ? (fnCall[parenDepth] || syntaxError(params), ",") // We don't allow top-level literal arrays or objects 1359 : lftPrn0 1360 ? "" 1361 : (aposed = apos, quoted = quot, '"') 1362 )) 1363 ); 1364 } 1365 } 1366 1367 var named, bindto, boundName, // list, 1368 tmplLinks = tmpl.links, 1369 fnCall = {}, 1370 pathStart = {0:-1}, 1371 parenDepth = 0, 1372 quoted = false, // boolean for string content in double quotes 1373 aposed = false; // or in single quotes 1374 1375 //pushBindings(); 1376 1377 return (params + " ") 1378 .replace(/\)\^/g, ").") // Treat "...foo()^bar..." as equivalent to "...foo().bar..." 1379 //since preceding computed observables in the path will always be updated if their dependencies change 1380 .replace(rParams, parseTokens); 1381 } 1382 1383 //========== 1384 // Utilities 1385 //========== 1386 1387 // Merge objects, in particular contexts which inherit from parent contexts 1388 function extendCtx(context, parentContext) { 1389 // Return copy of parentContext, unless context is defined and is different, in which case return a new merged context 1390 // If neither context nor parentContext are defined, return undefined 1391 return context && context !== parentContext 1392 ? (parentContext 1393 ? $extend($extend({}, parentContext), context) 1394 : context) 1395 : parentContext && $extend({}, parentContext); 1396 } 1397 1398 // Get character entity for HTML and Attribute encoding 1399 function getCharEntity(ch) { 1400 return charEntities[ch] || (charEntities[ch] = "&#" + ch.charCodeAt(0) + ";"); 1401 } 1402 1403 //========================== Initialize ========================== 1404 1405 for (jsvStoreName in jsvStores) { 1406 registerStore(jsvStoreName, jsvStores[jsvStoreName]); 1407 } 1408 1409 var $observable, 1410 $templates = $views.templates, 1411 $converters = $views.converters, 1412 $helpers = $views.helpers, 1413 $tags = $views.tags, 1414 $viewsSub = $views.sub, 1415 $viewsSettings = $views.settings; 1416 1417 if (jQuery) { 1418 //////////////////////////////////////////////////////////////////////////////////////////////// 1419 // jQuery is loaded, so make $ the jQuery object 1420 $ = jQuery; 1421 $.fn.render = renderContent; 1422 if ($observable = $.observable) { 1423 $extend($viewsSub, $observable.sub); // jquery.observable.js was loaded before jsrender.js 1424 delete $observable.sub; 1425 } 1426 } else { 1427 //////////////////////////////////////////////////////////////////////////////////////////////// 1428 // jQuery is not loaded. 1429 1430 $ = global.jsviews = {}; 1431 1432 $.isArray = Array && Array.isArray || function(obj) { 1433 return Object.prototype.toString.call(obj) === "[object Array]"; 1434 }; 1435 1436 // //========================== Future Node.js support ========================== 1437 // if ((nodeJsModule = global.module) && nodeJsModule.exports) { 1438 // nodeJsModule.exports = $; 1439 // } 1440 } 1441 1442 $.render = $render; 1443 $.views = $views; 1444 $.templates = $templates = $views.templates; 1445 1446 $viewsSettings({ 1447 debugMode: dbgMode, 1448 delimiters: $viewsDelimiters, 1449 _dbgMode: true, 1450 tryCatch: true 1451 }); 1452 1453 //========================== Register tags ========================== 1454 1455 $tags({ 1456 "else": function() {}, // Does nothing but ensures {{else}} tags are recognized as valid 1457 "if": { 1458 render: function(val) { 1459 // This function is called once for {{if}} and once for each {{else}}. 1460 // We will use the tag.rendering object for carrying rendering state across the calls. 1461 // If not done (a previous block has not been rendered), look at expression for this block and render the block if expression is truthy 1462 // Otherwise return "" 1463 var self = this, 1464 ret = (self.rendering.done || !val && (arguments.length || !self.tagCtx.index)) 1465 ? "" 1466 : (self.rendering.done = true, self.selected = self.tagCtx.index, 1467 // Test is satisfied, so render content on current context. We call tagCtx.render() rather than return undefined 1468 // (which would also render the tmpl/content on the current context but would iterate if it is an array) 1469 self.tagCtx.render()); 1470 return ret; 1471 },
1472 onUpdate: function(ev, eventArgs, tagCtxs) { 1473 var tci, prevArg, different; 1474 for (tci = 0; (prevArg = this.tagCtxs[tci]) && prevArg.args.length; tci++) { 1475 prevArg = prevArg.args[0]; 1476 different = !prevArg !== !tagCtxs[tci].args[0]; 1477 if (!!prevArg || different) { 1478 return different; 1479 // If newArg and prevArg are both truthy, return false to cancel update. (Even if values on later elses are different, we still don't want to update, since rendered output would be unchanged) 1480 // If newArg and prevArg are different, return true, to update 1481 // If newArg and prevArg are both falsey, move to the next {{else ...}} 1482 } 1483 } 1484 // Boolean value of all args are unchanged (falsey), so return false to cancel update 1485 return false; 1486 }, 1487 flow: true 1488 }, 1489 "for": { 1490 render: function(val) { 1491 // This function is called once for {{for}} and once for each {{else}}. 1492 // We will use the tag.rendering object for carrying rendering state across the calls. 1493 var self = this, 1494 tagCtx = self.tagCtx, 1495 result = "", 1496 done = 0; 1497 1498 if (!arguments.length) { 1499 val = tagCtx.view.data; // Defaults to current data. If current data is an array, iterates over data. (Use {{include}} to compose templates without array iteration) 1500 } 1501 1502 if (!self.rendering.done) { 1503 if (val !== undefined) { 1504 result += tagCtx.render(val); 1505 done += $.isArray(val) ? val.length : 1; 1506 } 1507 if (self.rendering.done = done) { 1508 self.selected = tagCtx.index; 1509 } 1510 // If nothing was rendered we will look at the next {{else}}. Otherwise, we are done. 1511 } 1512 return result; 1513 }, 1514 flow: true 1515 }, 1516 include: { 1517 flow: true 1518 }, 1519 "*": { 1520 // {{* code... }} - Ignored if template.allowCode is false. Otherwise include code in compiled template 1521 render: retVal, 1522 flow: true 1523 } 1524 }); 1525 1526 function getTargetProps(source) { 1527 // this pointer is theMap - which has tagCtx.props too 1528 // arguments: tagCtx.args. 1529 var key, prop, 1530 props = []; 1531 1532 if (typeof source === "object") { 1533 for (key in source) { 1534 prop = source[key]; 1535 if (!prop || !prop.toJSON || prop.toJSON()) { 1536 if (typeof prop !== "function") { 1537 props.push({ key: key, prop: source[key] }); 1538 } 1539 } 1540 } 1541 } 1542 return props; 1543 } 1544 1545 $tags({ 1546 props: $extend($extend({}, $tags["for"]), 1547 DataMap(getTargetProps) 1548 ) 1549 }); 1550 1551 //========================== Register converters ========================== 1552 1553 $converters({ 1554 html: function(text) { 1555 // HTML encode: Replace < > & and ' and " by corresponding entities. 1556 return text != undefined ? String(text).replace(rHtmlEncode, getCharEntity) : ""; // null and undefined return "" 1557 }, 1558 attr: function(text) { 1559 // Attribute encode: Replace < > & ' and " by corresponding entities. 1560 return text != undefined ? String(text).replace(rAttrEncode, getCharEntity) : text === null ? text : ""; // null returns null, e.g. to remove attribute. undefined returns "" 1561 }, 1562 url: function(text) { 1563 // URL encoding helper. 1564 return text != undefined ? encodeURI(String(text)) : text === null ? text : ""; // null returns null, e.g. to remove attribute. undefined returns "" 1565 } 1566 }); 1567 1568 //========================== Define default delimiters ========================== 1569 $viewsDelimiters(); 1570 1571})(this, this.jQuery);
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.