+++ /dev/null
-/*! JsRender v1.0pre: http://github.com/BorisMoore/jsrender */
-/*
-* Optimized version of jQuery Templates, for rendering to string.
-* Does not require jQuery, or HTML DOM
-* Integrates with JsViews (http://github.com/BorisMoore/jsviews)
-* Copyright 2012, Boris Moore
-* Released under the MIT License.
-*/
-// informal pre beta commit counter: 24
-
-(function(global, jQuery, undefined) {
- // global is the this object, which is window when running in the usual browser environment.
- "use strict";
-
- if (jQuery && jQuery.views || global.jsviews) { return; } // JsRender is already loaded
-
- //========================== Top-level vars ==========================
-
- var versionNumber = "v1.0pre",
-
- $, jsvStoreName, rTag, rTmplString,
-//TODO tmplFnsCache = {},
- delimOpenChar0 = "{", delimOpenChar1 = "{", delimCloseChar0 = "}", delimCloseChar1 = "}", linkChar = "^",
- FALSE = false, TRUE = true,
-
- rPath = /^(?:null|true|false|\d[\d.]*|([\w$]+|\.|~([\w$]+)|#(view|([\w$]+))?)([\w$.^]*?)(?:[.[^]([\w$]+)\]?)?)$/g,
- // object helper view viewProperty pathTokens leafToken
-
- rParams = /(\()(?=|\s*\()|(?:([([])\s*)?(?:([#~]?[\w$.^]+)?\s*((\+\+|--)|\+|-|&&|\|\||===|!==|==|!=|<=|>=|[<>%*!:?\/]|(=))\s*|([#~]?[\w$.^]+)([([])?)|(,\s*)|(\(?)\\?(?:(')|("))|(?:\s*([)\]])([([]?))|(\s+)/g,
- // lftPrn lftPrn2 path operator err eq path2 prn comma lftPrn2 apos quot rtPrn prn2 space
- // (left paren? followed by (path? followed by operator) or (path followed by paren?)) or comma or apos or quot or right paren or space
-
- rNewLine = /\r?\n/g,
- rUnescapeQuotes = /\\(['"])/g,
- // escape quotes and \ character
- rEscapeQuotes = /([\\'"])/g,
- rBuildHash = /\x08(~)?([^\x08]+)\x08/g,
- rTestElseIf = /^if\s/,
- rFirstElem = /<(\w+)[>\s]/,
- rPrevElem = /<(\w+)[^>\/]*>[^>]*$/,
- autoTmplName = 0,
- viewId = 0,
- escapeMapForHtml = {
- "&": "&",
- "<": "<",
- ">": ">"
- },
- attrEncodeChars = /[<"'&]/g,
- htmlEncodeChars = /[\x00<>"'&]/g,
- tmplAttr = "data-jsv-tmpl",
- fnDeclStr = "var j=j||" + (jQuery ? "jQuery." : "js") + "views,",
- slice = [].slice,
-
- $render = {},
- jsvStores = {
- template: {
- compile: compileTmpl
- },
- tag: {
- compile: compileTag
- },
- helper: {},
- converter: {}
- },
-
- // jsviews object ($.views if jQuery is loaded)
- $views = {
- jsviews: versionNumber,
- render: $render,
- View: View,
- settings: {
- delimiters: $viewsDelimiters,
- debugMode: TRUE,
- tryCatch: TRUE
- },
- sub: {
- // subscription, e.g. JsViews integration
- Error: JsViewsError,
- tmplFn: tmplFn,
- parse: parseParams,
- extend: $extend,
- error: error
-//TODO invoke: $invoke
- },
- _cnvt: convertVal,
- _tag: renderTag,
-
- // TODO provide better debug experience - e.g. support $.views.onError callback
- _err: function(e) {
- // Place a breakpoint here to intercept template rendering errors
- return $viewsSettings.debugMode ? ("Error: " + (e.message || e)) + ". " : '';
- }
- };
-
- function JsViewsError(message, object) {
- // Error exception type for JsViews/JsRender
- // Override of $.views.sub.Error is possible
- if (object && object.onError) {
- if (object.onError(message) === FALSE) {
- return;
- }
- }
- this.name = "JsRender Error";
- this.message = message || "JsRender error";
- }
-
- function $extend(target, source) {
- var name;
- target = target || {};
- for (name in source) {
- target[name] = source[name];
- }
- return target;
- }
-
-//TODO function $invoke() {
-// try {
-// return arguments[1].apply(arguments[0], arguments[2]);
-// }
-// catch(e) {
-// throw new $views.sub.Error(e, arguments[0]);
-// }
-// }
-
- (JsViewsError.prototype = new Error()).constructor = JsViewsError;
-
- //========================== Top-level functions ==========================
-
- //===================
- // jsviews.delimiters
- //===================
-
- function $viewsDelimiters(openChars, closeChars, link) {
- // Set the tag opening and closing delimiters and 'link' character. Default is "{{", "}}" and "^"
- // openChars, closeChars: opening and closing strings, each with two characters
-
- if (!$viewsSub.rTag || arguments.length) {
- delimOpenChar0 = openChars ? openChars.charAt(0) : delimOpenChar0; // Escape the characters - since they could be regex special characters
- delimOpenChar1 = openChars ? openChars.charAt(1) : delimOpenChar1;
- delimCloseChar0 = closeChars ? closeChars.charAt(0) : delimCloseChar0;
- delimCloseChar1 = closeChars ? closeChars.charAt(1) : delimCloseChar1;
- linkChar = link || linkChar;
- openChars = "\\" + delimOpenChar0 + "(\\" + linkChar + ")?\\" + delimOpenChar1; // Default is "{^{"
- closeChars = "\\" + delimCloseChar0 + "\\" + delimCloseChar1; // Default is "}}"
- // Build regex with new delimiters
- // tag (followed by / space or }) or cvtr+colon or html or code
- rTag = "(?:(?:(\\w+(?=[\\/\\s\\" + delimCloseChar0 + "]))|(?:(\\w+)?(:)|(>)|!--((?:[^-]|-(?!-))*)--|(\\*)))"
- + "\\s*((?:[^\\" + delimCloseChar0 + "]|\\" + delimCloseChar0 + "(?!\\" + delimCloseChar1 + "))*?)";
-
- // make rTag available to JsViews (or other components) for parsing binding expressions
- $viewsSub.rTag = rTag + ")";
-
- rTag = new RegExp(openChars + rTag + "(\\/)?|(?:\\/(\\w+)))" + closeChars, "g");
-
- // Default: bind tag converter colon html comment code params slash closeBlock
- // /{(\^)?{(?:(?:(\w+(?=[\/\s}]))|(?:(\w+)?(:)|(>)|!--((?:[^-]|-(?!-))*)--|(\*)))\s*((?:[^}]|}(?!}))*?)(\/)?|(?:\/(\w+)))}}/g
-
- rTmplString = new RegExp("<.*>|([^\\\\]|^)[{}]|" + openChars + ".*" + closeChars);
- // rTmplString looks for html tags or { or } char not preceded by \\, or JsRender tags {{xxx}}. Each of these strings are considered NOT to be jQuery selectors
- }
- return [delimOpenChar0, delimOpenChar1, delimCloseChar0, delimCloseChar1, linkChar];
- }
-
- //=========
- // View.get
- //=========
-
- function getView(type) {
- // TODO complete/test/provide samples for this
- // If type is undefined, returns root view (view under top view).
- var view = this,
- root = !type || type === "root";
- while (root && view.parent.parent || view && view.type !== type) {
- view = view.parent;
- }
- return view;
- }
-
- function getIndex() {
- var view = this.get("item");
- return view ? view.index : undefined;
- }
-
- getIndex.depends = function(view) {
- return [view.get("item"), "index"];
- }
- //==========
- // View._hlp
- //==========
-
- function getHelper(helper) {
- // Helper method called as view._hlp(key) from compiled template, for helper functions or template parameters ~foo
- var wrapped,
- view = this;
- if (helper = (view.ctx || {})[helper] || view.getRsc("helpers", helper)) {
- if (typeof helper === "function") {
- wrapped = function() {
- // If it is of type function, we will wrap it so it gets called with view as 'this' context.
- // If the helper ~foo() was in a data-link expression, the view will have a 'temporary' linkCtx property too.
- // However note that helper functions on deeper paths will not have access to view and tagCtx.
- // For example, ~util.foo() will have the ~util object as 'this' pointer
- return helper.apply(view, arguments);
- };
- $extend(wrapped, helper);
- }
- }
- return wrapped || helper;
- }
-
- //==============
- // jsviews._cnvt
- //==============
-
- function convertVal(converter, view, self, tagCtx, bindingPaths, text) {
- // self is template object or linkCtx object
- if (converter || bindingPaths) {
- var tmplConverter,
- linkCtx = !self.markup && self,
- tag = {
- tagName: converter + ":",
- tagCtx: tagCtx
- },
- args = tagCtx.args = slice.call(arguments, 5);
-
- tagCtx.view = view;
- tagCtx.bind = !!(linkCtx || bindingPaths);
-
- if (linkCtx) {
- linkCtx.tag = tag;
- tag.linkCtx = linkCtx;
- tagCtx.ctx = extendCtx(tagCtx.ctx, linkCtx.view.ctx);
- }
- tag.ctx = tagCtx.ctx || {};
- tagCtx.props = tagCtx.props || {};
- delete tagCtx.ctx;
-
- if (converter && ((tmplConverter = view.getRsc("converters", converter)) || error("Unknown converter: {{"+ converter + ":"))) {
- // A call to {{cnvt: ... }} or {^{cnvt: ... }} or data-link="{cnvt: ... }"
- text = tmplConverter.apply(tag, args);
- }
- if (bindingPaths) {
- // A call to {^{: ... }} or {^{cnvt: ... }}
- bindingPaths = view.tmpl.bnds[bindingPaths-1];
- linkCtx.paths = bindingPaths;
- // Consider being able to switch off binding if parent view is not currently bound.
- view._.tag = tag; // Provide this tag on view, for markerNode on bound tags, and for getting the tagCtx and linkCtx during rendering.
- // Provide this tag on view, for addMarkerNode on bound tags to add the tag to view._.bnds, associated with the tag id,
- // and so when rendering subsequent {{else}}, will be associated with this tag
- //TODO does this work with nested elses and with {^{foo:...}} which also adds tag to view, for markerNodes.
- text = view._.onRender(text, view, TRUE);
- //Example: text = '<script type="jsv123"></script>' + text + '<script type="jsv123/"></script>';
- }
- }
- return text;
- }
-
- //=============
- // jsviews._tag
- //=============
-
- function getResource(storeName, item) {
- var res,
- view = this,
- store = $views[storeName];
-
- res = store && store[item];
- while (!res && view) {
- store = view.tmpl[storeName];
- res = store && store[item];
- view = view.parent;
- }
- return res;
- }
-
- function getResource2(storeName, item, root) {
- var view = this,
- store = !root && $views[storeName];
- return store && store[item]
- || (store = view.tmpl[storeName], store && store[item])
- || view.parent && view.parent.getRsc(storeName, item, TRUE);
- }
-
- function renderTag(tagName, parentView, self, content, tagCtx, bind) {
- // Called from within compiled template function, to render a template tag
- // Returns the rendered tag
-
- var ret, render, ctx, elses, tag, tags,
- tmpl = self.markup && self,
- // self is either a template object (if rendering a tag) or a linkCtx object (if linking using a link tag)
- linkCtx = !tmpl && self,
- parentView_ = parentView._,
- parentTmpl = tmpl || parentView.tmpl,
- childTemplates = parentTmpl.templates,
- tagDef = parentView.getRsc("tags", tagName) || error("Unknown tag: {{"+ tagName + "}}"),
- args = tagCtx.args = arguments.length > 6 ? slice.call(arguments, 6) : [],
- props = tagCtx.props = tagCtx.props || {};
-
- tagCtx.view = parentView;
- tagCtx.ctx = extendCtx(tagCtx.ctx, parentView.ctx); // Extend parentView.ctx
- ctx = tagCtx.ctx || {};
- delete tagCtx.ctx;
-
- // Set the tmpl property to the content of the block tag, unless set as an override property on the tag
- tmpl = props.tmpl;
- content = content && parentTmpl.tmpls[content - 1];
- tmpl = tmpl || content || tagDef.template || undefined;
- tmpl = "" + tmpl === tmpl // if a string
- ? parentView.getRsc("templates", tmpl) || $templates(tmpl)
- : tmpl;
-
- if (tagName === "else") {
- tag = parentView._.tag;
- // Switch current tagCtx of tag instance to this {{else ...}}
- elses = tag._elses = tag._elses || [];
- elses.push(tmpl);
- tagCtx.isElse = elses.length;
- render = tag.render;
- }
- if (tagDef.init) {
- // init is the constructor for the tag/control instance
-
- // tags hash: tag.ctx.tags, merged with parentView.ctx.tags,
- tags = ctx.tags = parentView.ctx && extendCtx(ctx.tags, parentView.ctx.tags) || {};
-
- tag = tag || linkCtx.tag;
- if (tag) {
- // tag has already been instantiated, so keep it, but attach the current context, which may have changed
- // Add tag to tags hash
- tags[tagName] = tag;
- } else {
- // If the tag has not already been instantiated, we will create a new instance and add to the tags hash,
- // so ~tags.tagName will access the tag, even within the rendering of the template content of this tag
-// TODO provide error handling owned by the tag - using tag.onError
-// try {
- tag = tags[tagName] = new tagDef.init(tagCtx, linkCtx, ctx);
-// }
-// catch(e) {
-// tagDef.onError(e);
-// }
- tag.tmpl = tmpl;
-
- if (linkCtx) {
- tag.attr =
- // Setting attr on tag so renderContent knows whether to include script node markers.
- linkCtx.attr =
- // Setting attr on self to ensure outputting to the correct target attribute.
- linkCtx.attr || tagDef.attr || "";
- }
- }
- ctx.tag = tag;
- } else {
- // This is a simple tag declared as a function. We won't instantiate a specific tag constructor - just a standard instance object.
- tag = tag || {
- // tag instance object if no init constructor
- render: tagDef.render,
- renderContent: renderContent,
- tmpl: tmpl,
- tagName: tagName
- };
- }
-
- // Provide tagCtx, linkCtx and ctx access from tag
- tag.tagCtx = tagCtx;
- tag.ctx = ctx;
- if (linkCtx) {
- linkCtx.tag = tag;
- tag.linkCtx = linkCtx;
- }
-
- tag._is = "tag";
- tag._done = tagCtx.isElse ? tag._done : FALSE; // If not an {{else}} this is a new
- tmpl = tmpl || tag.tmpl;
- elses = tag._elses;
-
-//TODO The above works for initial rendering, but when refreshing {^{foo}} need also to associate with {{else}} tags. Use compilation to bind else content templates and expressions with the primary tag template and expression.
-
- parentView_.tag = tag;
- // Provide this tag on view, for addMarkerNode on bound tags to add the tag to view._.bnds, associated with the tag id,
- // for getting the tagCtx and linkCtx during rendering, and so when rendering subsequent {{else}}, will be associated with this tag
- //TODO does this work with nested elses and with {^{foo:...}} which also adds tag to view, for markerNodes.
-
-// while (tmpl) {
- // If tagDef has a 'render' function, call it.
- // If the return result is undefined, return "", or, if a template (or content) is provided,
- // return the rendered template(using the current data or the first parameter as data);
- if (render = render || tag.render) {
- ret = render.apply(tag, args);
-
-// TODO ret = $invoke(tag, render, args);
- }
- ret = ret !== undefined
- ? ret // Return result of render function unless it is undefined, in which case return rendered template
- : tmpl
- // render template on args[0] if defined, or otherwise on the current data item
- ? tag.renderContent(tagCtx.data !== undefined ? tagCtx.data : parentView.data, undefined, parentView)
- : ""; // No return value from render, and no template defined, so return ::
-
-// tmpl = (tag !== "else" && elses) ? (tagCtx.isElse = tagCtx.isElse || 0, elses[tagCtx.isElse++]) : undefined;
-//}
-
- // If bind, for {^{tag ... }}, insert script marker nodes
- return bind ? parentView_.onRender(ret, parentView, bind) : ret;
- }
-
- //=================
- // View constructor
- //=================
-
- function View(context, type, parentView, data, template, key, onRender) {
- // Constructor for view object in view hierarchy. (Augmented by JsViews if JsViews is loaded)
- var views, parentView_,
- isArray = type === "array",
- self_ = {
- key: 0,
- useKey: isArray ? 0 : 1,
- id: "" + viewId++,
- onRender: onRender,
- bnd: {}
- },
- self = {
- data: data,
- tmpl: template,
- views: isArray ? [] : {},
- parent: parentView,
- ctx: context,
- type: type,
- // If the data is an array, this is an 'array view' with a views array for each child 'item view'
- // If the data is not an array, this is an 'item view' with a views 'map' object for any child nested views
- // ._.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
- get: getView,
- getIndex: getIndex,
- getRsc: getResource,
- _hlp: getHelper,
- _: self_
- };
-
- if (parentView) {
- views = parentView.views;
- parentView_ = parentView._;
- if (parentView_.useKey) {
- // Parent is an 'item view'. Add this view to its views object
- // self._key = is the key in the parent view map
- views[self_.key = "_" + parentView_.useKey++] = self;
- } else {
- // Parent is an 'array view'. Add this view to its views array
- views.splice(
- // self._.key = self.index - the index in the parent view array
- self_.key = self.index =
- key !== undefined
- ? key
- : views.length,
- 0, self);
- }
- // If no context was passed in, use parent context
- // If context was passed in, it should have been merged already with parent context
- self.ctx = context || parentView.ctx;
- }
- return self;
- }
-
- //=============
- // Registration
- //=============
-
- function compileChildResources(parentTmpl) {
- var storeName, resources, resourceName, settings, compile;
- for (storeName in jsvStores) {
- settings = jsvStores[storeName];
- if ((compile = settings.compile) && (resources = parentTmpl[storeName + "s"])) {
- for (resourceName in resources) {
- // compile child resource declarations (templates, tags, converters or helpers)
- resources[resourceName] = compile(resourceName, resources[resourceName], parentTmpl, storeName, settings);
- }
- }
- }
- }
-
- function compileTag(name, item, parentTmpl) {
- var init, tmpl;
- if (typeof item === "function") {
- // Simple tag declared as function. No presenter instantation.
- item = {
- tagName: name,
- render: item,
- depends: item.depends
- };
- } else {
- // Tag declared as object, used as the prototype for tag instantiation (control/presenter)
- item.tagName = name;
- if (tmpl = item.template) {
- item.template = "" + tmpl === tmpl ? ($templates[tmpl] || $templates(tmpl)) : tmpl;
- }
- if (item.init !== FALSE) {
- init = item.init = item.init || function(tagCtx) {};
- init.prototype = item;
- (init.prototype = item).constructor = init;
- }
- }
- item.renderContent = renderContent;
- item.attr = "html";
- if (parentTmpl) {
- item._parentTmpl = parentTmpl;
- }
-//TODO item.onError = function(e) {
-// var error;
-// if (error = this.prototype.onError) {
-// error.call(this, e);
-// } else {
-// throw e;
-// }
-// }
- return item;
- }
-
- function compileTmpl(name, tmpl, parentTmpl, storeName, storeSettings, options) {
- // tmpl is either a template object, a selector for a template script block, the name of a compiled template, or a template object
-
- //==== nested functions ====
- function tmplOrMarkupFromStr(value) {
- // If value is of type string - treat as selector, or name of compiled template
- // Return the template object, if already compiled, or the markup string
-
- if (("" + value === value) || value.nodeType > 0) {
- try {
- elem = value.nodeType > 0
- ? value
- : !rTmplString.test(value)
- // If value is a string and does not contain HTML or tag content, then test as selector
- && jQuery && jQuery(value)[0];
- // If selector is valid and returns at least one element, get first element
- // If invalid, jQuery will throw. We will stay with the original string.
- } catch (e) { }
-
- if (elem) {
- // Generally this is a script element.
- // However we allow it to be any element, so you can for example take the content of a div,
- // use it as a template, and replace it by the same content rendered against data.
- // e.g. for linking the content of a div to a container, and using the initial content as template:
- // $.link("#content", model, {tmpl: "#content"});
-
- value = elem.getAttribute(tmplAttr);
- name = name || value;
- value = $templates[value];
- if (!value) {
- // Not already compiled and cached, so compile and cache the name
- // Create a name for compiled template if none provided
- name = name || "_" + autoTmplName++;
- elem.setAttribute(tmplAttr, name);
- value = $templates[name] = compileTmpl(name, elem.innerHTML, parentTmpl, storeName, storeSettings, options); // Use tmpl as options
- }
- }
- return value;
- }
- // If value is not a string, return undefined
- }
-
- var tmplOrMarkup, elem;
-
- //==== Compile the template ====
- tmpl = tmpl || "";
- tmplOrMarkup = tmplOrMarkupFromStr(tmpl);
-
- // If options, then this was already compiled from a (script) element template declaration.
- // If not, then if tmpl is a template object, use it for options
- options = options || (tmpl.markup ? tmpl : {});
- options.tmplName = name;
- if (parentTmpl) {
- options._parentTmpl = parentTmpl;
- }
- // If tmpl is not a markup string or a selector string, then it must be a template object
- // In that case, get it from the markup property of the object
- if (!tmplOrMarkup && tmpl.markup && (tmplOrMarkup = tmplOrMarkupFromStr(tmpl.markup))) {
- if (tmplOrMarkup.fn && (tmplOrMarkup.debug !== tmpl.debug || tmplOrMarkup.allowCode !== tmpl.allowCode)) {
- // if the string references a compiled template object, but the debug or allowCode props are different, need to recompile
- tmplOrMarkup = tmplOrMarkup.markup;
- }
- }
- if (tmplOrMarkup !== undefined) {
- if (name && !parentTmpl) {
- $render[name] = function() {
- return tmpl.render.apply(tmpl, arguments);
- };
- }
- if (tmplOrMarkup.fn || tmpl.fn) {
- // tmpl is already compiled, so use it, or if different name is provided, clone it
- if (tmplOrMarkup.fn) {
- if (name && name !== tmplOrMarkup.tmplName) {
- tmpl = extendCtx(options, tmplOrMarkup);
- } else {
- tmpl = tmplOrMarkup;
- }
- }
- } else {
- // tmplOrMarkup is a markup string, not a compiled template
- // Create template object
- tmpl = TmplObject(tmplOrMarkup, options);
- // Compile to AST and then to compiled function
- tmplFn(tmplOrMarkup, tmpl);
- }
- compileChildResources(options);
- return tmpl;
- }
- }
- //==== /end of function compile ====
-
- function TmplObject(markup, options) {
- // Template object constructor
- var htmlTag,
- wrapMap = $viewsSettings.wrapMap || {},
- tmpl = $extend(
- {
- markup: markup,
- tmpls: [],
- links: {},
- bnds: [],
- render: renderContent
- },
- options
- );
-
- if (!options.htmlTag) {
- // Set tmpl.tag to the top-level HTML tag used in the template, if any...
- htmlTag = rFirstElem.exec(markup);
- tmpl.htmlTag = htmlTag ? htmlTag[1].toLowerCase() : "";
- }
- htmlTag = wrapMap[tmpl.htmlTag];
- if (htmlTag && htmlTag !== wrapMap.div) {
- // When using JsViews, we trim templates which are inserted into HTML contexts where text nodes are not rendered (i.e. not 'Phrasing Content').
- tmpl.markup = $.trim(tmpl.markup);
- tmpl._elCnt = TRUE; // element content model (no rendered text nodes), not phrasing content model
- }
-
- return tmpl;
- }
-
- function registerStore(storeName, storeSettings) {
-
- function theStore(name, item, parentTmpl) {
- // The store is also the function used to add items to the store. e.g. $.templates, or $.views.tags
-
- // For store of name 'thing', Call as:
- // $.views.things(items[, parentTmpl]),
- // or $.views.things(name, item[, parentTmpl])
-
- var onStore, compile, items, itemName, childTemplates, childTemplate, thisStore, childStoreName;
-
- if (name && "" + name !== name && !name.nodeType && !name.markup) {
- // Call to $.views.things(items[, parentTmpl]),
-
- // Adding items to the store
- // If name is a map, then item is parentTmpl. Iterate over map and call store for key.
- for (itemName in name) {
- theStore(itemName, name[itemName], item);
- }
- return $views;
- }
- thisStore = parentTmpl ? parentTmpl[storeNames] = parentTmpl[storeNames] || {} : theStore;
-
- // Adding a single unnamed item to the store
- if (item === undefined) {
- item = name;
- name = undefined;
- }
- compile = storeSettings.compile;
- if (onStore = $viewsSub.onBeforeStoreItem) {
- // e.g. provide an external compiler or preprocess the item.
- compile = onStore(thisStore, name, item, compile) || compile;
- }
- if (!name) {
- item = compile(undefined, item);
- } else if ("" + name === name) { // name must be a string
- if (item === null) {
- // If item is null, delete this entry
- delete thisStore[name];
- } else {
- thisStore[name] = compile ? (item = compile(name, item, parentTmpl, storeName, storeSettings)) : item;
- }
- }
- if (item) {
- item._is = storeName;
- }
- if (onStore = $viewsSub.onStoreItem) {
- // e.g. JsViews integration
- onStore(thisStore, name, item, compile);
- }
- return item;
- }
-
- var storeNames = storeName + "s";
-
- $views[storeNames] = theStore;
- jsvStores[storeName] = storeSettings;
- }
-
- //==============
- // renderContent
- //==============
-
- function renderContent(data, context, parentView, key, isLayout, onRender) {
- // Render template against data as a tree of subviews (nested rendered template instances), or as a string (top-level template).
- // If the data is the parent view, treat as layout template, re-render with the same data context.
- var i, l, dataItem, newView, childView, itemResult, parentContext, props, swapContent, tagCtx, isTag, outerOnRender,
- self = this,
- tmpl = self,
- allowDataLink = self.attr === undefined || self.attr === "html",
- result = "";
-
- if (key === TRUE) {
- swapContent = TRUE;
- key = 0;
- }
- if (isTag = self._is === "tag") {
- tagCtx = self.tagCtx;
- // This is a call from renderTag
- tmpl = tagCtx.isElse ? self._elses[tagCtx.isElse-1] : self.tmpl;
- context = extendCtx(context, self.ctx);
- props = tagCtx.props;
- if ( props.link === FALSE ) {
- // link=false setting on block tag
- // We will override inherited value of link by the explicit setting link=false taken from props
- // The child views of an unlinked view are also unlinked. So setting child back to true will not have any effect.
- context = context || {};
- context.link = FALSE;
- }
- parentView = parentView || tagCtx.view;
- } else {
- tmpl = self.jquery && (self[0] || error('Unknown template: "' + self.selector + '"')) // This is a call from $(selector).render
- || self;
- }
- if (tmpl) {
- if (parentView) {
- onRender = onRender || parentView._.onRender;
- parentContext = parentView.ctx;
- if (data === parentView) {
- // Inherit the data from the parent view.
- // This may be the contents of an {{if}} block
- // Set isLayout = true so we don't iterate the if block if the data is an array.
- data = parentView.data;
- isLayout = TRUE;
- }
- }
-
- // Set additional context on views created here, (as modified context inherited from the parent, and to be inherited by child views)
- // Note: If no jQuery, $extend does not support chained copies - so limit extend() to two parameters
- context = extendCtx(context, parentContext);
-
- if (!tmpl.fn) {
- tmpl = $templates[tmpl] || $templates(tmpl);
- }
-
- if (tmpl) {
- onRender = (context && context.link) !== FALSE && allowDataLink && onRender;
- // If link===false, do not call onRender, so no data-linking marker nodes
- outerOnRender = onRender;
- if (onRender === TRUE) {
- // Used by view.refresh(). Don't create a new wrapper view.
- outerOnRender = undefined;
- onRender = parentView._.onRender;
- }
- if ($.isArray(data) && !isLayout) {
- // Create a view for the array, whose child views correspond to each data item. (Note: if key and parentView are passed in
- // along with parent view, treat as insert -e.g. from view.addViews - so parentView is already the view item for array)
- newView = swapContent ? parentView : (key !== undefined && parentView) || View(context, "array", parentView, data, tmpl, key, onRender);
- for (i = 0, l = data.length; i < l; i++) {
- // Create a view for each data item.
- dataItem = data[i];
- childView = View(context, "item", newView, dataItem, tmpl, (key || 0) + i, onRender);
- itemResult = tmpl.fn(dataItem, childView, $views);
- result += newView._.onRender ? newView._.onRender(itemResult, childView) : itemResult;
- }
- } else {
- // Create a view for singleton data object. The type of the view will be the tag name, e.g. "if" or "myTag" except for
- // "item", "array" and "data" views. A "data" view is from programatic render(object) against a 'singleton'.
- newView = swapContent ? parentView : View(context, self.tagName||"data", parentView, data, tmpl, key, onRender);
- result += tmpl.fn(data, newView, $views);
- }
- return outerOnRender ? outerOnRender(result, newView) : result;
- }
- }
- return "";
- }
-
- //===========================
- // Build and compile template
- //===========================
-
- // Generate a reusable function that will serve to render a template against data
- // (Compile AST then build template function)
-
- function error(message) {
- if ($viewsSettings.debugMode) {
- throw new $views.sub.Error(message);
- }
- }
-
- function syntaxError(message) {
- error("Syntax error\n" + message);
- }
-
- function tmplFn(markup, tmpl, isLinkExpression) {
- // Compile markup to AST (abtract syntax tree) then build the template function code from the AST nodes
- // Used for compiling templates, and also by JsViews to build functions for data link expressions
-
-
- //==== nested functions ====
- function pushprecedingContent(shift) {
- shift -= loc;
- if (shift) {
- content.push(markup.substr(loc, shift).replace(rNewLine, "\\n"));
- }
- }
-
- function blockTagCheck(tagName) {
- tagName && syntaxError('Unmatched or missing tag: "{{/' + tagName + '}}" in template:\n' + markup);
- }
-
- function parseTag(all, bind, tagName, converter, colon, html, comment, codeTag, params, slash, closeBlock, index) {
-
- // bind tag converter colon html comment code params slash closeBlock
- // /{(\^)?{(?:(?:(\w+(?=[\/\s}]))|(?:(\w+)?(:)|(>)|!--((?:[^-]|-(?!-))*)--|(\*)))\s*((?:[^}]|}(?!}))*?)(\/)?|(?:\/(\w+)))}}/g
- // Build abstract syntax tree (AST): [ tagName, converter, params, content, hash, bindings, contentMarkup ]
- if (html) {
- colon = ":";
- converter = "html";
- }
- var noError, current0,
- pathBindings = [],
- code = "",
- hash = "",
- passedCtx = "",
- // Block tag if not self-closing and not {{:}} or {{>}} (special case) and not a data-link expression
- block = !slash && !colon && !comment && !isLinkExpression;
-
- //==== nested helper function ====
- tagName = tagName || colon;
- pushprecedingContent(index);
- loc = index + all.length; // location marker - parsed up to here
- if (codeTag) {
- if (allowCode) {
- content.push(["*", "\n" + params.replace(rUnescapeQuotes, "$1") + "\n"]);
- }
- } else if (tagName) {
- if (tagName === "else") {
- if (rTestElseIf.test(params)) {
- syntaxError('for "{{else if expr}}" use "{{else expr}}"');
- }
- current[7] = markup.substring(current[7], index); // contentMarkup for block tag
- current = stack.pop();
- content = current[3];
- block = TRUE;
- }
- if (params) {
- params = params.replace(/\s*\n\s*/g, " "); // remove newlines from the params string, to avoid compiled code errors for unterminated strings
- code = parseParams(params, pathBindings)
- .replace(rBuildHash, function(all, isCtx, keyValue) {
- if (isCtx) {
- passedCtx += keyValue + ",";
- } else {
- hash += keyValue + ",";
- }
- return "";
- });
- }
- hash = hash.slice(0, -1);
- code = code.slice(0, -1);
- noError = hash && (hash.indexOf("noerror:true") + 1) && hash || "";
-
- newNode = [
- tagName,
- converter || "",
- code,
- block && [],
- "{" + (hash ? ("props:" + (noError ? "hsh": "{" + hash + "}")
- + ",") : "") + 'params:"' + params + '"' + (passedCtx ? ",ctx:{" + passedCtx.slice(0, -1) + "}" : "") + "},",
- noError,
- //"{" + (hash ? ("props:{" + hash + "},") : "") + 'params:"' + params + '"' + (passedCtx ? ",ctx:{" + passedCtx.slice(0, -1) + "}" : "") + "},",
- bind && pathBindings || 0,
- ];
- content.push(newNode);
- if (block) {
- stack.push(current);
- current = newNode;
- current[7] = loc; // Store current location of open tag, to be able to add contentMarkup when we reach closing tag
- }
- } else if (closeBlock) {
- current0 = current[0];
- blockTagCheck(closeBlock !== current0 && current0 && current0 !== "else");
- current[7] = markup.substring(current[7], index); // contentMarkup for block tag
- current = stack.pop();
- }
- blockTagCheck(!current && closeBlock);
- content = current[3];
- }
- //==== /end of nested functions ====
-
- var newNode,
- allowCode = tmpl && tmpl.allowCode,
- astTop = [],
- loc = 0,
- stack = [],
- content = astTop,
- current = [, , , astTop];
-
- markup = markup.replace(rEscapeQuotes, "\\$1");
-
-//TODO result = tmplFnsCache[markup]; // Only cache if template is not named and markup length < ..., and there are no bindings or subtemplates?? Consider standard optimization for data-link="a.b.c"
-// if (result) {
-// tmpl.fn = result;
-// } else {
-
-// result = markup;
-
- blockTagCheck(stack[0] && stack[0][3].pop()[0]);
-
- // Build the AST (abstract syntax tree) under astTop
- markup.replace(rTag, parseTag);
-
- pushprecedingContent(markup.length);
-
- if (loc = astTop[astTop.length-1]) {
- blockTagCheck("" + loc !== loc && (+loc[7] === loc[7]) && loc[0]);
- }
-// result = tmplFnsCache[markup] = buildCode(astTop, tmpl);
-// }
- return buildCode(astTop, tmpl);
- }
-
- function buildCode(ast, tmpl) {
- // Build the template function code from the AST nodes, and set as property on the passed-in template object
- // Used for compiling templates, and also by JsViews to build functions for data link expressions
- var ret, i, node, hasTag, noError, hasEncoder, getsValue, hasConverter, hasViewPath, tagName, converter, params, hash, bindings, bindingPaths, nestedTmpls, nestedTmpl, allowCode, content, markup,
- code = "",
- tmplOptions = {},
- l = ast.length;
-
- if (tmpl) {
- if (allowCode = tmpl.allowCode) {
- tmplOptions.allowCode = TRUE;
- }
- if (tmpl.debug) {
- tmplOptions.debug = TRUE;
- }
- bindings = tmpl.bnds;
- nestedTmpls = tmpl.tmpls;
- }
-
- for (i = 0; i < l; i++) {
- // AST nodes: [ tagName, converter, params, content, hash, bindings, contentMarkup, link ]
- node = ast[i];
-
- // Add newline for each callout to t() c() etc. and each markup string
- ret = "";
- if ("" + node === node) {
- // a markup string to be inserted
- ret = 'ret+="' + node + '";';
- } else {
- // a compiled tag expression to be inserted
- tagName = node[0];
- if (tagName === "*") {
- // Code tag: {{* }}
- ret = "" + node[1];
- } else {
- converter = node[1];
- params = node[2];
- content = node[3];
- hash = node[4];
- noError = node[5];
- bindingPaths = node[6];
- markup = node[7];
-
- if (content) {
- // Create template object for nested template
- nestedTmpl = TmplObject(markup, tmplOptions);
- // Compile to AST and then to compiled function
- buildCode(content, nestedTmpl);
- nestedTmpls.push(nestedTmpl);
- }
- if (bindingPaths) {
- // Add leaf binding paths to template
- bindings.push(bindingPaths);
- bindingPaths = bindings.length;
- }
- hasViewPath = hasViewPath || hash.indexOf("view") > -1;
- // Add newline for each callout to t() c() etc.
-
- //TODO consider passing in ret to c() and t() so they can look at the previous ret, and detect whether this is a jsrender tag _within_an_HTML_element_tag_
- // and if so, don't insert marker nodes, add data-link attributes to the HTML element markup... No need for people to set link=false.
-
- if (noError) {
- // If the tag includes noerror=true, we will do a try catch around expressions for named or unnamed parameters
- // passed to the tag, and return the empty string for each expression if it throws during evaluation
- // TODO perhaps support noerror=xxx and return the value of the expression xxx||'', rather than always the empty string
- noError = "try{prm=" + params + ";hsh={" + noError + '};}catch(e){prm="";hsh={};}\n';
- params = "prm";
- }
-
- ret += noError + "ret+=" + (tagName === ":"
- ? (converter === "html" && !bindingPaths
- ? (hasEncoder = TRUE, "h(" + params+ ");")
- : converter || bindingPaths // Call _cnvt if there is a converter, or binding: {{cnvt: ... }}, {^{: ... }} or {^{cnvt: ... }}
- ? (hasConverter = TRUE, 'c("' + converter + '",view,this,' + hash + bindingPaths + "," + params + ");")
- : (getsValue = TRUE, "(v=" + params + ')!=u?v:"";')
- )
- : (hasTag = TRUE, 't("' + tagName + '",view,this,'
- + (content ? nestedTmpls.length : '""') // For block tags, pass in the key (nestedTmpls.length) to the nested content template
- + "," + hash + bindingPaths + (params ? "," : "") + params) + ");");
- }
- }
- code += "\n" + ret;
- }
-
- // Include only the var references that are needed in the code
- code = fnDeclStr
- + (noError? "prm,hsh," : "")
- + (getsValue ? "v," : "")
- + (hasTag ? "t=j._tag," : "")
- + (hasConverter ? "c=j._cnvt," : "")
- + (hasEncoder ? "h=j.converters.html," : "")
- + 'ret="";\n'
- + ($viewsSettings.tryCatch ? "try{\n" : "")
- + (tmplOptions.debug ? "debugger;" : "")
- + code + "\nreturn ret;\n"
- + ($viewsSettings.tryCatch ? "\n}catch(e){return j._err(e);}" : "");
-
- try {
- code = new Function("data, view, j, u", code);
- } catch (e) {
- syntaxError("Compiled template code:\n\n" + code, e);
- }
-
- if (tmpl) {
- tmpl.fn = code;
- }
- return code;
- }
-
- function parseParams(params, bindings) {
-
- function parseTokens(all, lftPrn0, lftPrn, path, operator, err, eq, path2, prn, comma, lftPrn2, apos, quot, rtPrn, prn2, space) {
- // rParams = /(\()(?=|\s*\()|(?:([([])\s*)?(?:([#~]?[\w$^.]+)?\s*((\+\+|--)|\+|-|&&|\|\||===|!==|==|!=|<=|>=|[<>%*!:?\/]|(=))\s*|([#~]?[\w$^.]+)([([])?)|(,\s*)|(\(?)\\?(?:(')|("))|(?:\s*([)\]])([([]?))|(\s+)
- // lftPrn0-flwed by (- lftPrn path operator err eq path2 prn comma lftPrn3 apos quot rtPrn prn2 space
- // (left paren? followed by (path? followed by operator) or (path followed by paren?)) or comma or apos or quot or right paren or space
- operator = operator || "";
- lftPrn = lftPrn || lftPrn0 || lftPrn2;
- path = path || path2;
- prn = prn || prn2 || "";
-
- function parsePath(all, object, helper, view, viewProperty, pathTokens, leafToken) {
- // rPath = /^(?:null|true|false|\d[\d.]*|([\w$]+|~([\w$]+)|#(view|([\w$]+))?)([\w$.^]*?)(?:[.[^]([\w$]+)\]?)?)$/g,
- // object helper view viewProperty pathTokens leafToken
-
- if (object) {
- bindings.push(path);
- if (object !== ".") {
- var leaf,
- ret = (helper
- ? 'view._hlp("' + helper + '")'
- : view
- ? "view"
- : "data")
- + (leafToken
- ? (viewProperty
- ? "." + viewProperty
- : helper
- ? ""
- : (view ? "" : "." + object)
- ) + (pathTokens || "")
- : (leafToken = helper ? "" : view ? viewProperty || "" : object, ""));
-
- leaf = (leafToken ? "." + leafToken : "");
- ret = ret + leaf;
- ret = ret.slice(0, 9) === "view.data"
- ? ret.slice(5) // convert #view.data... to data...
- : ret;
- return ret;
- }
- }
- return all;
- }
-
- if (err) {
- syntaxError(params);
- } else {
- return (aposed
- // within single-quoted string
- ? (aposed = !apos, (aposed ? all : '"'))
- : quoted
- // within double-quoted string
- ? (quoted = !quot, (quoted ? all : '"'))
- :
- (
- (lftPrn
- ? (parenDepth++, lftPrn)
- : "")
- + (space
- ? (parenDepth
- ? ""
- : named
- ? (named = FALSE, "\b")
- : ","
- )
- : eq
- // named param
- // Insert backspace \b (\x08) as separator for named params, used subsequently by rBuildHash
- ? (parenDepth && syntaxError(params), named = TRUE, '\b' + path + ':')
- : path
- // path
- ? (path.split("^").join(".").replace(rPath, parsePath)
- + (prn
- ? (fnCall[++parenDepth] = TRUE, prn)
- : operator)
- )
- : operator
- ? operator
- : rtPrn
- // function
- ? ((fnCall[parenDepth--] = FALSE, rtPrn)
- + (prn
- ? (fnCall[++parenDepth] = TRUE, prn)
- : "")
- )
- : comma
- ? (fnCall[parenDepth] || syntaxError(params), ",") // We don't allow top-level literal arrays or objects
- : lftPrn0
- ? ""
- : (aposed = apos, quoted = quot, '"')
- ))
- );
- }
- }
- var named,
- fnCall = {},
- parenDepth = 0,
- quoted = FALSE, // boolean for string content in double quotes
- aposed = FALSE; // or in single quotes
-
- bindings.expr = params.replace(rUnescapeQuotes, "$1");
- return (params + " ").replace(rParams, parseTokens);
- }
-
- //==========
- // Utilities
- //==========
-
- // HTML encoding helper
- function replacerForHtml(ch) {
- // Original code from Mike Samuel <msamuel@google.com>
- return escapeMapForHtml[ch]
- // Intentional assignment that caches the result of encoding ch.
- || (escapeMapForHtml[ch] = "&#" + ch.charCodeAt(0) + ";");
- }
-
- // Merge objects, in particular contexts which inherit from parent contexts
- function extendCtx(context, parentContext) {
- // Return copy of parentContext, unless context is defined and is different, in which case return a new merged context
- // If neither context nor parentContext are undefined, return undefined
- return context && context !== parentContext
- ? (parentContext
- ? $extend($extend({}, parentContext), context)
- : context)
- : parentContext && $extend({}, parentContext);
- }
-
- //========================== Initialize ==========================
-
- for (jsvStoreName in jsvStores) {
- registerStore(jsvStoreName, jsvStores[jsvStoreName]);
- }
-
- var $templates = $views.templates,
- $converters = $views.converters,
- $helpers = $views.helpers,
- $tags = $views.tags,
- $viewsSub = $views.sub,
- $viewsSettings = $views.settings;
-
- if (jQuery) {
- ////////////////////////////////////////////////////////////////////////////////////////////////
- // jQuery is loaded, so make $ the jQuery object
- $ = jQuery;
- $.render = $render;
- $.views = $views;
- $.templates = $templates = $views.templates;
- $.fn.render = renderContent;
-
- } else {
- ////////////////////////////////////////////////////////////////////////////////////////////////
- // jQuery is not loaded.
-
- $ = global.jsviews = $views;
-
- $.isArray = Array && Array.isArray || function(obj) {
- return Object.prototype.toString.call(obj) === "[object Array]";
- };
- }
-
- //========================== Register tags ==========================
-
- $tags({
- "if": function(val) {
- var self = this;
- // If not done and val is truey, set done=true on tag instance and render content. Otherwise return ""
- // On else will call this function again on the same tag instance.
- return (self._done || arguments.length && !val)
- ? ""
- : (self._done = true,
- // Test is satisfied, so render content on current context. Rather than return undefined
- // (which will render the tmpl/content on the current context but will iterate if it is an array),
- // we pass in the view. This ensures treating as a layout template - with no iteration
- self.renderContent(self.tagCtx.view));
- },
-// Temporary fix for binding to {{if}}
-// "if": {
-// render: function(val) {
-// var self = this;
-// return (self._done || arguments.length && !val) ? "" : (self._done = true, self.renderContent(self.tagCtx.view));
-// }
-// },
- "else": function() {}, // Does nothing but ensures {{else}} tags are recognized as valid
- "for": function() {
- var i, arg, undef,
- self = this,
- tagCtx = self.tagCtx,
- result = "",
- args = arguments,
- done = 0,
- l = args.length;
-
- if (!l) {
- return tagCtx.done
- ? ""
- : (tagCtx.done = TRUE,
- // Test is satisfied, so render content on current context. Rather than return undefined
- // (which will render the tmpl/content on the current context but will iterate if it is an array),
- // we pass in the view. This ensures treating as a layout template - with no iteration
- self.renderContent(tagCtx.view));
- }
- for (i = 0; i < l; i++) {
- arg = args[i];
- undef = arg === undefined;
- if (!undef) {
- done += $.isArray(arg) ? arg.length : 1;
- result += self.renderContent(arg);
- } else {
- return "";
- }
- }
- tagCtx.done = done;
- return result;
- },
- "*": function(value) {
- return value;
- }
- });
-
- //========================== Register global helpers ==========================
-
- // $helpers({ // Global helper functions
- // // TODO add any useful built-in helper functions
- // });
-
- //========================== Register converters ==========================
-
- $converters({
- html: function(text) {
- // HTML encoding helper: Replace < > & and ' and " by corresponding entities.
- return text != undefined ? String(text).replace(htmlEncodeChars, replacerForHtml) : "";
- },
- attr: function(text) {
- // Attribute encoding helper: Replace < & ' and " by corresponding entities.
- return text != undefined ? String(text).replace(attrEncodeChars, replacerForHtml) : "";
- },
- url: function(text) {
- // TODO - support chaining {{attr|url:....}} to protect against injection attacks from url parameters containing " or '.
- // URL encoding helper.
- return text != undefined ? encodeURI(String(text)) : "";
- }
- });
-
- //========================== Define default delimiters ==========================
- $viewsDelimiters();
-
-})(this, this.jQuery);