forerunnerdb
Version:
A NoSQL document store database for browsers and Node.js.
1,341 lines (1,197 loc) • 207 kB
JavaScript
var init = (function () {
/*! jsviews.js v1.0.0-alpha single-file version:
includes JsRender, JsObservable and JsViews http://github.com/BorisMoore/jsrender and http://jsviews.com/jsviews
informal pre V1.0 commit counter: 60 (Beta Candidate) */
/* JsRender:
* See http://github.com/BorisMoore/jsrender and http://jsviews.com/jsrender
* Copyright 2014, Boris Moore
* Released under the MIT License.
*/
(function(global, jQuery, undefined) {
// global is the this object, which is window when running in the usual browser environment.
"use strict";
if (jQuery && jQuery.render || global.jsviews) { return; } // JsRender is already loaded
//========================== Top-level vars ==========================
var versionNumber = "v1.0.0-beta",
$, jsvStoreName, rTag, rTmplString, indexStr, // nodeJsModule,
//TODO tmplFnsCache = {},
delimOpenChar0 = "{", delimOpenChar1 = "{", delimCloseChar0 = "}", delimCloseChar1 = "}", linkChar = "^",
rPath = /^(!*?)(?:null|true|false|\d[\d.]*|([\w$]+|\.|~([\w$]+)|#(view|([\w$]+))?)([\w$.^]*?)(?:[.[^]([\w$]+)\]?)?)$/g,
// none object helper view viewProperty pathTokens leafToken
rParams = /(\()(?=\s*\()|(?:([([])\s*)?(?:(\^?)(!*?[#~]?[\w$.^]+)?\s*((\+\+|--)|\+|-|&&|\|\||===|!==|==|!=|<=|>=|[<>%*:?\/]|(=))\s*|(!*?[#~]?[\w$.^]+)([([])?)|(,\s*)|(\(?)\\?(?:(')|("))|(?:\s*(([)\]])(?=\s*\.|\s*\^|\s*$)|[)\]])([([]?))|(\s+)/g,
// lftPrn0 lftPrn bound path operator err eq path2 prn comma lftPrn2 apos quot rtPrn rtPrnDot prn2 space
// (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
rNewLine = /[ \t]*(\r\n|\n|\r)/g,
rUnescapeQuotes = /\\(['"])/g,
rEscapeQuotes = /['"\\]/g, // Escape quotes and \ character
rBuildHash = /(?:\x08|^)(onerror:)?(?:(~?)(([\w$]+):)?([^\x08]+))\x08(,)?([^\x08]+)/gi,
rTestElseIf = /^if\s/,
rFirstElem = /<(\w+)[>\s]/,
rAttrEncode = /[\x00`><"'&]/g, // Includes > encoding since rConvertMarkers in JsViews does not skip > characters in attribute strings
rIsHtml = /[\x00`><\"'&]/,
rHasHandlers = /^on[A-Z]|^convert(Back)?$/,
rHtmlEncode = rAttrEncode,
autoTmplName = 0,
viewId = 0,
charEntities = {
"&": "&",
"<": "<",
">": ">",
"\x00": "�",
"'": "'",
'"': """,
"`": "`"
},
htmlStr = "html",
tmplAttr = "data-jsv-tmpl",
$render = {},
jsvStores = {
template: {
compile: compileTmpl
},
tag: {
compile: compileTag
},
helper: {},
converter: {}
},
// jsviews object ($.views if jQuery is loaded)
$views = {
jsviews: versionNumber,
settings: function(settings) {
$extend($viewsSettings, settings);
dbgMode($viewsSettings._dbgMode);
if ($viewsSettings.jsv) {
$viewsSettings.jsv();
}
},
sub: {
// subscription, e.g. JsViews integration
View: View,
Err: JsViewsError,
tmplFn: tmplFn,
cvt: convertArgs,
parse: parseParams,
extend: $extend,
syntaxErr: syntaxError,
onStore: {},
_lnk: retVal,
_ths: tagHandlersFromProps
},
map: dataMap, // If jsObservable loaded first, use that definition of dataMap
_cnvt: convertVal,
_tag: renderTag,
_err: error
};
function tagHandlersFromProps(tag, tagCtx) {
for (var prop in tagCtx.props) {
if (rHasHandlers.test(prop)) {
tag[prop] = tagCtx.props[prop]; // Copy over the onFoo props, convert and convertBack from tagCtx.props to tag (overrides values in tagDef).
// Note: unsupported scenario: if handlers are dynamically added ^onFoo=expression this will work, but dynamically removing will not work.
}
}
}
function retVal(val) {
return val;
}
function dbgBreak(val) {
debugger; // Insert breakpoint for debugging JsRender or JsViews.
// Consider https://github.com/BorisMoore/jsrender/issues/239: eval("debugger; //dbg"); // Insert breakpoint for debugging JsRender or JsViews. Using eval to prevent issue with minifiers (YUI Compressor)
return val;
}
function dbgMode(debugMode) {
$viewsSettings._dbgMode = debugMode;
indexStr = debugMode ? "Unavailable (nested view): use #getIndex()" : ""; // If in debug mode set #index to a warning when in nested contexts
$tags("dbg", $helpers.dbg = $converters.dbg = debugMode ? dbgBreak : retVal); // Register {{dbg/}}, {{dbg:...}} and ~dbg() to insert break points for debugging - if in debug mode.
}
function JsViewsError(message) {
// Error exception type for JsViews/JsRender
// Override of $.views.sub.Error is possible
this.name = ($.link ? "JsViews" : "JsRender") + " Error";
this.message = message || this.name;
}
function $extend(target, source) {
var name;
for (name in source) {
target[name] = source[name];
}
return target;
}
function $isFunction(ob) {
return typeof ob === "function";
}
(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 (!$sub.rTag || openChars) {
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
$sub.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(inner, type) { //view.get(inner, type)
if (!type) {
// view.get(type)
type = inner;
inner = undefined;
}
var views, i, l, found,
view = this,
root = !type || type === "root";
// If type is undefined, returns root view (view under top view).
if (inner) {
// Go through views - this one, and all nested ones, depth-first - and return first one with given type.
found = view.type === type ? view : undefined;
if (!found) {
views = view.views;
if (view._.useKey) {
for (i in views) {
if (found = views[i].get(inner, type)) {
break;
}
}
} else {
for (i = 0, l = views.length; !found && i < l; i++) {
found = views[i].get(inner, type);
}
}
}
} else if (root) {
// Find root view. (view whose parent is top view)
while (view.parent.parent) {
found = view = view.parent;
}
} else {
while (view && !found) {
// Go through views - this one, and all parent ones - and return first one with given type.
found = view.type === type ? view : undefined;
view = view.parent;
}
}
return found;
}
function getNestedIndex() {
var view = this.get("item");
return view ? view.index : undefined;
}
getNestedIndex.depends = function() {
return [this.get("item"), "index"];
};
function getIndex() {
return this.index;
}
getIndex.depends = function() {
return ["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,
ctx = view.linkCtx,
res = (view.ctx || {})[helper];
if (res === undefined && ctx && ctx.ctx) {
res = ctx.ctx[helper];
}
if (res === undefined) {
res = $helpers[helper];
}
if (res) {
if ($isFunction(res) && !res._wrp) {
wrapped = function() {
// 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
// view as 'this' context. If the helper ~foo() was in a data-link expression, the view will have a 'temporary' linkCtx property too.
// Note that helper functions on deeper paths will have specific this pointers, from the preceding path.
// For example, ~util.foo() will have the ~util object as 'this' pointer
return res.apply((!this || this === global) ? view : this, arguments);
};
wrapped._wrp = true;
$extend(wrapped, res); // Attach same expandos (if any) to the wrapped function
}
}
return wrapped || res;
}
//==============
// jsviews._cnvt
//==============
function convertVal(converter, view, tagCtx, onError) {
// self is template object or linkCtx object
var tag, value,
// if tagCtx is an integer, then it is the key for the compiled function to return the boundTag tagCtx
boundTag = +tagCtx === tagCtx && view.tmpl.bnds[tagCtx-1],
linkCtx = view.linkCtx; // For data-link="{cvt:...}"...
onError = onError !== undefined && {props: {}, args: [onError]};
tagCtx = onError || (boundTag ? boundTag(view.data, view, $views) : tagCtx);
value = tagCtx.args[0];
if (converter || boundTag) {
tag = linkCtx && linkCtx.tag;
if (!tag) {
tag = {
_: {
inline: !linkCtx,
bnd: boundTag
},
tagName: ":",
cvt: converter,
flow: true,
tagCtx: tagCtx,
_is: "tag"
};
if (linkCtx) {
linkCtx.tag = tag;
tag.linkCtx = linkCtx;
tagCtx.ctx = extendCtx(tagCtx.ctx, linkCtx.view.ctx);
}
$sub._lnk(tag);
}
tag._er = onError && value;
tagHandlersFromProps(tag, tagCtx);
tagCtx.view = view;
tag.ctx = tagCtx.ctx || {};
delete tagCtx.ctx;
// Provide this tag on view, for addBindingMarkers on bound tags to add the tag to view._.bnds, associated with the tag id,
view._.tag = tag;
value = convertArgs(tag, tag.convert || converter !== "true" && converter)[0]; // If there is a convertBack but no convert, converter will be "true"
// Call onRender (used by JsViews if present, to add binding annotations around rendered content)
value = boundTag && view._.onRender
? view._.onRender(value, view, boundTag)
: value;
view._.tag = undefined;
}
return value != undefined ? value : "";
}
function convertArgs(tag, converter) {
var tagCtx = tag.tagCtx,
view = tagCtx.view,
args = tagCtx.args;
converter = converter && ("" + converter === converter
? (view.getRsc("converters", converter) || error("Unknown converter: '" + converter + "'"))
: converter);
args = !args.length && !tagCtx.index // On the opening tag with no args, bind to the current data context
? [view.data]
: converter
? args.slice() // If there is a converter, use a copy of the tagCtx.args array for rendering, and replace the args[0] in
// the copied array with the converted value. But we do not modify the value of tag.tagCtx.args[0] (the original args array)
: args; // If no converter, render with the original tagCtx.args
if (converter) {
if (converter.depends) {
tag.depends = $sub.getDeps(tag.depends, tag, converter.depends, converter);
}
args[0] = converter.apply(tag, args);
}
return args;
}
//=============
// jsviews._tag
//=============
function getResource(resourceType, itemName) {
var res, store,
view = this;
while ((res === undefined) && view) {
store = view.tmpl[resourceType];
res = store && store[itemName];
view = view.parent;
}
return res || $views[resourceType][itemName];
}
function renderTag(tagName, parentView, tmpl, tagCtxs, isUpdate, onError) {
// Called from within compiled template function, to render a template tag
// Returns the rendered tag
var tag, tags, attr, parentTag, i, l, itemRet, tagCtx, tagCtxCtx, content, tagDef,
callInit, mapDef, thisMap, args, props, initialTmpl,
ret = "",
linkCtx = parentView.linkCtx || 0,
ctx = parentView.ctx,
parentTmpl = tmpl || parentView.tmpl,
// if tagCtx is an integer, then it is the key for the compiled function to return the boundTag tagCtxs
boundTag = +tagCtxs === tagCtxs && parentTmpl.bnds[tagCtxs-1];
if (tagName._is === "tag") {
tag = tagName;
tagName = tag.tagName;
tagCtxs = tag.tagCtxs;
}
tag = tag || linkCtx.tag;
onError = onError !== undefined && (ret += onError, [{props: {}, args: []}]);
tagCtxs = onError || (boundTag ? boundTag(parentView.data, parentView, $views) : tagCtxs);
l = tagCtxs.length;
for (i = 0; i < l; i++) {
if (!i && (!tmpl || !tag)) {
tagDef = parentView.getRsc("tags", tagName) || error("Unknown tag: {{" + tagName + "}}");
}
tagCtx = tagCtxs[i];
if (!linkCtx.tag || tag._er) {
// We are initializing tag, so for block tags, tagCtx.tmpl is an integer > 0
content = tagCtx.tmpl;
content = tagCtx.content = content && parentTmpl.tmpls[content - 1];
$extend(tagCtx, {
tmpl: (tag ? tag : tagDef).template || content, // Set the tmpl property to the content of the block tag
render: renderContent,
index: i,
view: parentView,
ctx: extendCtx(tagCtx.ctx, ctx) // Extend parentView.ctx
// Possible future feature:
//var updatedValueOfArg0 = this.tagCtx.get(0);
//var updatedValueOfPropFoo = this.tagCtx.get("foo");
//var updatedValueOfCtxPropFoo = this.tagCtx.get("~foo");
//_fns: {},
//get: function(key) {
// return (this._fns[key] = this._fns[key] || new Function("data,view,j,u",
// "return " + $.views.sub.parse(this.params[+key === key ? "args" : (key.charAt(0) === "~" ? (key = key.slice(1), "ctx") : "props")][key]) + ";")
// )(this.view.data, this.view, $views);
//},
});
}
if (tmpl = tagCtx.props.tmpl) {
// If the tmpl property is overridden, set the value (when initializing, or, in case of binding: ^tmpl=..., when updating)
tmpl = "" + tmpl === tmpl // if a string
? parentView.getRsc("templates", tmpl) || $templates(tmpl)
: tmpl;
tagCtx.tmpl = tmpl;
}
if (!tag) {
// This will only be hit for initial tagCtx (not for {{else}}) - if the tag instance does not exist yet
// Instantiate tag if it does not yet exist
if (tagDef._ctr) {
// If the tag has not already been instantiated, we will create a new instance.
// ~tag will access the tag, even within the rendering of the template content of this tag.
// From child/descendant tags, can access using ~tag.parent, or ~parentTags.tagName
tag = new tagDef._ctr();
callInit = !!tag.init;
} else {
// 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.
$sub._lnk(tag = {
// tag instance object if no init constructor
render: tagDef.render
});
}
tag._ = {
inline: !linkCtx
};
if (linkCtx) {
linkCtx.tag = tag;
tag.linkCtx = linkCtx;
}
if (tag._.bnd = boundTag || linkCtx.fn) {
// Bound if {^{tag...}} or data-link="{tag...}"
tag._.arrVws = {};
} else if (tag.dataBoundOnly) {
error("{^{" + tagName + "}} tag must be data-bound");
}
tag.tagName = tagName;
tag.parent = parentTag = ctx && ctx.tag;
tag._is = "tag";
tag._def = tagDef;
tag.tagCtxs = tagCtxs;
//TODO better perf for childTags() - keep child tag.tags array, (and remove child, when disposed)
// tag.tags = [];
// Provide this tag on view, for addBindingMarkers on bound tags to add the tag to view._.bnds, associated with the tag id
}
tagCtx.tag = tag;
if (tag.dataMap && tag.tagCtxs) {
tagCtx.map = tag.tagCtxs[i].map; // Copy over the compiled map instance from the previous tagCtxs to the refreshed ones
}
if (!tag.flow) {
tagCtxCtx = tagCtx.ctx = tagCtx.ctx || {};
// tags hash: tag.ctx.tags, merged with parentView.ctx.tags,
tags = tag.parents = tagCtxCtx.parentTags = ctx && extendCtx(tagCtxCtx.parentTags, ctx.parentTags) || {};
if (parentTag) {
tags[parentTag.tagName] = parentTag;
//TODO better perf for childTags: parentTag.tags.push(tag);
}
tags[tag.tagName] = tagCtxCtx.tag = tag;
}
}
parentView._.tag = tag;
if (!(tag._er = onError)) {
tagHandlersFromProps(tag, tagCtxs[0]);
tag.rendering = {}; // Provide object for state during render calls to tag and elses. (Used by {{if}} and {{for}}...)
for (i = 0; i < l; i++) {
tagCtx = tag.tagCtx = tag.tagCtxs[i];
props = tagCtx.props;
args = convertArgs(tag, tag.convert);
if (mapDef = props.dataMap || tag.dataMap) {
if (args.length || props.dataMap) {
thisMap = tagCtx.map;
if (!thisMap || thisMap.src !== args[0] || isUpdate) {
if (thisMap && thisMap.src) {
thisMap.unmap(); // only called if observable map - not when only used in JsRender, e.g. by {{props}}
}
thisMap = tagCtx.map = mapDef.map(args[0], props);
}
args = [thisMap.tgt];
}
}
tag.ctx = tagCtx.ctx;
if (!i && callInit) {
initialTmpl = tag.template;
tag.init(tagCtx, linkCtx, tag.ctx);
callInit = undefined;
if (tag.template !== initialTmpl) {
tag._.tmpl = tag.template; // This will override the tag.template and also tagCtx.props.tmpl for all tagCtxs
}
if (linkCtx) {
// Set attr on linkCtx to ensure outputting to the correct target attribute.
// Setting either linkCtx.attr or this.attr in the init() allows per-instance choice of target attrib.
linkCtx.attr = tag.attr = linkCtx.attr || tag.attr;
}
}
itemRet = undefined;
if (tag.render) {
itemRet = tag.render.apply(tag, args);
}
args = args.length ? args : [parentView]; // no arguments - get data context from view.
itemRet = itemRet !== undefined
? itemRet // Return result of render function unless it is undefined, in which case return rendered template
: tagCtx.render(args[0], true) || (isUpdate ? undefined : "");
// No return value from render, and no template/content tagCtx.render(...), so return undefined
ret = ret ? ret + (itemRet || "") : itemRet; // If no rendered content, this will be undefined
}
delete tag.rendering;
}
tag.tagCtx = tag.tagCtxs[0];
tag.ctx = tag.tagCtx.ctx;
if (tag._.inline && (attr = tag.attr) && attr !== htmlStr) {
// inline tag with attr set to "text" will insert HTML-encoded content - as if it was element-based innerText
ret = attr === "text"
? $converters.html(ret)
: "";
}
return boundTag && parentView._.onRender
// Call onRender (used by JsViews if present, to add binding annotations around rendered content)
? parentView._.onRender(ret, parentView, boundTag)
: ret;
}
//=================
// View constructor
//=================
function View(context, type, parentView, data, template, key, contentTmpl, onRender) {
// Constructor for view object in view hierarchy. (Augmented by JsViews if JsViews is loaded)
var views, parentView_, tag,
self = this,
isArray = type === "array",
self_ = {
key: 0,
useKey: isArray ? 0 : 1,
id: "" + viewId++,
onRender: onRender,
bnds: {}
};
self.data = data;
self.tmpl = template,
self.content = contentTmpl;
self.views = isArray ? [] : {};
self.parent = parentView;
self.type = type || "top";
// 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 'hash' object for any child nested views
// ._.useKey is non zero if is not an 'array view' (owning a data array). Use this as next key for adding to child views hash
self._ = self_;
self.linked = !!onRender;
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 hash
views[self_.key = "_" + parentView_.useKey++] = self;
self.index = indexStr;
self.getIndex = getNestedIndex;
tag = parentView_.tag;
self_.bnd = isArray && (!tag || !!tag._.bnd && tag); // For array views that are data bound for collection change events, set the
// 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 ...}}
} 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,
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;
} else {
self.ctx = context;
}
}
View.prototype = {
get: getView,
getIndex: getIndex,
getRsc: getResource,
hlp: getHelper,
_is: "view"
};
//=============
// Registration
//=============
function compileChildResources(parentTmpl) {
var storeName, resources, resourceName, resource, settings, compile, onStore;
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, tags["for"] or helpers)
resource = resources[resourceName] = compile(resourceName, resources[resourceName], parentTmpl);
if (resource && (onStore = $sub.onStore[storeName])) {
// e.g. JsViews integration
onStore(resourceName, resource, compile);
}
}
}
}
}
function compileTag(name, tagDef, parentTmpl) {
var init, tmpl;
if ($isFunction(tagDef)) {
// Simple tag declared as function. No presenter instantation.
tagDef = {
depends: tagDef.depends,
render: tagDef
};
} else {
if (tagDef.baseTag) {
tagDef.flow = !!tagDef.flow; // default to false even if baseTag has flow=true
tagDef = $extend($extend({}, tagDef.baseTag), tagDef);
}
// Tag declared as object, used as the prototype for tag instantiation (control/presenter)
if ((tmpl = tagDef.template) !== undefined) {
tagDef.template = "" + tmpl === tmpl ? ($templates[tmpl] || $templates(tmpl)) : tmpl;
}
if (tagDef.init !== false) {
// Set int: false on tagDef if you want to provide just a render method, or render and template, but no constuctor or prototype.
// so equivalent to setting tag to render function, except you can also provide a template.
init = tagDef._ctr = function() {};
(init.prototype = tagDef).constructor = init;
}
}
if (parentTmpl) {
tagDef._parentTmpl = parentTmpl;
}
return tagDef;
}
function compileTmpl(name, tmpl, parentTmpl, 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(global.document).find(value)[0]; // TODO address case where DOM is not available
// 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 = $templates[name = name || elem.getAttribute(tmplAttr)];
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);
// Use tmpl as options
value = $templates[name] = compileTmpl(name, elem.innerHTML, parentTmpl, options);
}
elem = undefined;
}
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.replace(rEscapeQuotes, "\\$&"), tmpl);
}
compileChildResources(options);
return tmpl;
}
}
function dataMap(mapDef) {
function newMap(source, options) {
this.tgt = mapDef.getTgt(source, options);
}
if ($isFunction(mapDef)) {
// Simple map declared as function
mapDef = {
getTgt: mapDef
};
}
if (mapDef.baseMap) {
mapDef = $extend($extend({}, mapDef.baseMap), mapDef);
}
mapDef.map = function(source, options) {
return new newMap(source, options);
};
return mapDef;
}
//==== /end of function compile ====
function TmplObject(markup, options) {
// Template object constructor
var htmlTag,
wrapMap = $viewsSettings.wrapMap || {},
tmpl = $extend(
{
markup: markup,
tmpls: [],
links: {}, // Compiled functions for link expressions
tags: {}, // Compiled functions for bound tag expressions
bnds: [],
_is: "template",
render: fastRender
},
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').
// Currently not trimmed for <li> tag. (Not worth adding perf cost)
tmpl.markup = $.trim(tmpl.markup);
}
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, itemName, thisStore;
if (name && typeof name === "object" && !name.nodeType && !name.markup && !name.getTgt) {
// Call to $.views.things(items[, parentTmpl]),
// Adding items to the store
// If name is a hash, then item is parentTmpl. Iterate over hash and call store for key.
for (itemName in name) {
theStore(itemName, name[itemName], item);
}
return $views;
}
// Adding a single unnamed item to the store
if (item === undefined) {
item = name;
name = undefined;
}
if (name && "" + name !== name) { // name must be a string
parentTmpl = item;
item = name;
name = undefined;
}
thisStore = parentTmpl ? parentTmpl[storeNames] = parentTmpl[storeNames] || {} : theStore;
compile = storeSettings.compile;
if (item === null) {
// If item is null, delete this entry
name && delete thisStore[name];
} else {
item = compile ? (item = compile(name, item, parentTmpl)) : item;
name && (thisStore[name] = item);
}
if (compile && item) {
item._is = storeName; // Only do this for compiled objects (tags, templates...)
}
if (item && (onStore = $sub.onStore[storeName])) {
// e.g. JsViews integration
onStore(name, item, compile);
}
return item;
}
var storeNames = storeName + "s";
$views[storeNames] = theStore;
jsvStores[storeName] = storeSettings;
}
//==============
// renderContent
//==============
function $fastRender(data, context, noIteration) {
var tmplElem = this.jquery && (this[0] || error('Unknown template: "' + this.selector + '"')),
tmpl = tmplElem.getAttribute(tmplAttr);
return fastRender.call(tmpl ? $templates[tmpl] : $templates(tmplElem), data, context, noIteration);
}
function tryFn(tmpl, data, view) {
if ($viewsSettings._dbgMode) {
try {
return tmpl.fn(data, view, $views);
}
catch (e) {
return error(e, view);
}
}
return tmpl.fn(data, view, $views);
}
function fastRender(data, context, noIteration, parentView, key, onRender) {
var self = this;
if (!parentView && self.fn._nvw && !$.isArray(data)) {
return tryFn(self, data, {tmpl: self});
}
return renderContent.call(self, data, context, noIteration, parentView, key, onRender);
}
function renderContent(data, context, noIteration, parentView, key, 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 noIteration, re-render with the same data context.
var i, l, dataItem, newView, childView, itemResult, swapContent, tagCtx, contentTmpl, tag_, outerOnRender, tmplName, tmpl, noViews,
self = this,
result = "";
if (!!context === context) {
noIteration = context; // passing boolean as second param - noIteration
context = undefined;
}
if (key === true) {
swapContent = true;
key = 0;
}
if (self.tag) {
// This is a call from renderTag or tagCtx.render(...)
tagCtx = self;
self = self.tag;
tag_ = self._;
tmplName = self.tagName;
tmpl = tag_.tmpl || tagCtx.tmpl;
noViews = self.attr && self.attr !== htmlStr,
context = extendCtx(context, self.ctx);
contentTmpl = tagCtx.content; // The wrapped content - to be added to views, below
if (tagCtx.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;
data = arguments.length ? data : parentView;
} else {
tmpl = self;
}
if (tmpl) {
if (!parentView && data && data._is === "view") {
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
}
if (parentView) {
contentTmpl = contentTmpl || parentView.content; // The wrapped content - to be added as #content property on views, below
onRender = onRender || parentView._.onRender;
if (data === parentView) {
// Inherit the data from the parent view.
// This may be the contents of an {{if}} block
data = parentView.data;
}
context = extendCtx(context, parentView.ctx);
}
if (!parentView || parentView.type === "top") {
(context = context || {}).root = data; // Provide ~root as shortcut to top-level data.
}
// 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
if (!tmpl.fn) {
tmpl = $templates[tmpl] || $templates(tmpl);
}
if (tmpl) {
onRender = (context && context.link) !== false && !noViews && 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;
}
context = tmpl.helpers
? extendCtx(tmpl.helpers, context)
: context;
if ($.isArray(data) && !noIteration) {
// 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) || new View(context, "array", parentView, data, tmpl, key, contentTmpl, onRender);
for (i = 0, l = data.length; i < l; i++) {
// Create a view for each data item.
dataItem = data[i];
childView = new View(context, "item", newView, dataItem, tmpl, (key || 0) + i, contentTmpl, onRender);
itemResult = tryFn(tmpl, dataItem, childView);
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 programmatic render(object) against a 'singleton'.
if (parentView || !tmpl.fn._nvw) {
newView = swapContent ? parentView : new View(context, tmplName || "data", parentView, data, tmpl, key, contentTmpl, onRender);
if (tag_ && !self.flow) {
newView.tag = self;
}
}
result += tryFn(tmpl, data, newView);
}
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(e, view, fallback) {
var message = $viewsSettings.onError(e, view, fallback);
if ("" + e === e) { // if e is a string, not an Exception, then throw new Exception
throw new $sub.Err(message);
}
return !view.linkCtx && view.linked ? $converters.html(message) : message;
}
function syntaxError(message) {
error("Syntax error\n" + message);
}
function tmplFn(markup, tmpl, isLinkExpr, convertBack) {
// 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 = htmlStr;
}
slash = slash || isLinkExpr;
var pathBindings = (bind || isLinkExpr) && [[]],
props = "",
args = "",
ctxProps = "",
paramsArgs = "",
paramsProps = "",
paramsCtxProps = "",
onError = "",
useTrigger = "",
// Block tag if not self-closing and not {{:}} or {{>}} (special case) and not a data-link expression
block = !slash && !colon && !comment;
//==== nested helper function ====
tagName = tagName || (params = params || "#data", colon); // {{:}} is equivalent to {{:#data}}
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}}"');
}
pathBindings = current[7];
current[8] = markup.substring(current[8], index); // contentMarkup for block tag
current = stack.pop();
content = current[2];
block = true;
}
if (params) {
// remove newlines from the params string, to avoid compiled code errors for unterminated strings
parseParams(params.replace(rNewLine, " "), pathBindings, tmpl)
.replace(rBuildHash, function(all, onerror, isCtx, key, keyToken, keyValue, arg, param) {
if (arg) {
args += keyValue + ",";
paramsArgs += "'" + param + "',";
} else if (isCtx) {
ctxProps += key + keyValue + ",";
paramsCtxProps += key + "'" + param + "',";
} else if (onerror) {
onError += keyValue;
} else {
if (keyToken === "trigger") {
useTrigger += keyValue;
}
props += key + keyValue + ",";
paramsProps += key + "'" + param + "',";
hasHandlers = hasHandlers || rHasHandlers.test(keyToken);
}
return "";
}).slice(0, -1);
if (pathBindings && pathBindings[0]) {
pathBindings.pop(); // Remove the bindings that was prepared for next arg. (There is always an extra one ready).
}
}
newNode = [
tagName,
converter || !!convertBack || hasHandlers || "",
block && [],
parsedParam(paramsArgs, paramsProps, paramsCtxProps),
parsedParam(args, props, ctxProps),
onError,
useTrigger,
pathBindings || 0
];
content.push(newNode);
if (block) {
stack.push(current);
current = newNode;
current[8] = loc; // Store current location of open tag, to be able to add contentMarkup when we reach closing tag
}
} else if (closeBlock) {
blockTagCheck(closeBlock !== current[0] && current[0] !== "else" && closeBlock);
current[8] = markup.substring(current[8], index); // contentMarkup for block tag
current = stack.pop();
}
blockTagCheck(!current && closeBlock);
content = current[2];
}
//==== /end of nested functions ====
var result, newNode, hasHandlers,
allowCode = tmpl && tmpl.allowCode,
astTop = [],
loc = 0,
stack = [],
content = astTop,
current = [,,astTop];
//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;
if (isLinkExpr) {
markup = delimOpenChar0 + markup + delimCloseChar1;
}
blockTagCheck(stack[0] && stack[0][2].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[8] === loc[8]) && loc[0]);
}
// result = tmplFnsCache[markup] = buildCode(astTop, tmpl);
// }
if (isLinkExpr) {
result = buildCode(astTop, markup, isLinkExpr);
setPaths(result, astTop[0][7]); // With data-link expressions, pathBindings array is astTop[0][7]
} else {
result = buildCode(astTop, tmpl);
}
if (result._nvw) {
result._nvw = !/[~#]/.test(markup);
}
return result;
}
function setPaths(fn, paths) {
fn.deps = [];
for (var key in paths) {
if (key !== "_jsvto" && paths[key].length) {
fn.deps = fn.deps.concat(paths[key]);
}
}
fn.paths = paths;
}
function parsedParam(args, props, ctx) {
return [args.slice(0, -1), props.slice(0, -1), ctx.slice(0, -1)];
}
function paramStructure(parts, type) {
return '\n\t' + (type ? type + ':{' : '') + 'args:[' + parts[0] + ']' + (parts[1] || !type ? ',\n\tprops:{' + parts[1] + '}' : "") + (parts[2] ? ',\n\tctx:{' + parts[2] + '}' : "");
}
function parseParams(params, pathBindings, tmpl) {
function parseTokens(all, lftPrn0, lftPrn, bound, path, operator, err, eq, path2, prn, comma, lftPrn2, apos, quot, rtPrn, rtPrnDot, prn2, space, index, full) {
//rParams = /(\()(?=\s*\()|(?:([([])\s*)?(?:(\^?)(!*?[#~]?[\w$.^]+)?\s*((\+\+|--)|\+|-|&&|\|\||===|!==|==|!=|<=|>=|[<>%*:?\/]|(=))\s*|(!*?[#~]?[\w$.^]+)([([])?)|(,\s*)|(\(?)\\?(?:(')|("))|(?:\s*(([)\]])(?=\s*\.|\s*\^)|[)\]])([([]?))|(\s+)/g,
// lftPrn0 lftPrn bound path operator err eq path2 prn comma lftPrn2 apos quot rtPrn rtPrnDot 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 || "";
var expr, isFn, exprFn,
fullLength = full.length - 1;
function parsePath(allPath, not, object, helper, view, viewProperty, pathTokens, leafToken) {
// rPath = /^(?:null|true|false|\d[\d.]*|(!*?)([\w$]+|\.|~([\w$]+)|#(view|([\w$]+))?)([\w$.^]*?)(?:[.[^]([\w$]+)\]?)?)$/g,
// none object helper view viewProperty pathTokens leafToken
if (object) {
if (bindings) {
if (named === "linkTo") {
bindto = pathBindings._jsvto = pathBindings._jsvto || [];
bindto.push(path);
}
if (!named || boundName) {
bindings.push(path.slice(not.length)); // Add path binding for paths on props and args
}
}
if (object !== ".") {
var ret = (helper
? 'view.hlp("' + helper + '")'
: view
? "view"
: "data")
+ (leafToken
? (viewProperty
? "." + viewProperty
: helper
? ""
: (view ? "" : "." + object)
) + (pathTokens || "")
: (leafToken = helper ? "" : view ? viewProperty || "" : object, ""));
ret = ret + (leafToken ? "." + leafToken : "");
return not + (ret.slice(0, 9) === "view.data"
? ret.slice(5) // convert #view.data... to data...
: ret);
}
}
return allPath;
}
if (err && !aposed && !quoted) {
syntaxError(params);
} else {
if (bindings && rtPrnDot && !aposed && !quoted) {
// 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
// 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)
if (!named || boundName || bindto) {
expr = pathStart[parenDepth];
if (fullLength > index - expr) { // We need to compile a subexpression
expr = full.slice(expr, index + 1);
rtPrnDot = delimOpenChar1 + ":" + expr // The parameter or function subexpression
+ " onerror=''" // set onerror='' in order to wrap generated code with a try catch - returning '' as object instance if there is an error/missing parent
+ delimCloseChar0;
exprFn = tmplLinks[rtPrnDot];
if (!exprFn) {
tmplLinks[rtPrnDot] = true; // Flag that this exprFn (for rtPrnDot) is being compiled
tmplLinks[rtPrnDot] = exprFn = tmplFn(rtPrnDot, tmpl || bindings, true); // Compile the expression (or use cached copy already in tmpl.links)
exprFn.paths.push({_jsvOb: exprFn}); //list.push({_jsvOb: rtPrnDot});
}
if (exprFn !== true) { // If not reentrant call during compilation
(bindto || bindings).push({_jsvOb: exprFn}); // Insert special object for in path bindings, to be used for binding the compiled sub expression ()
}
}
}
}
return (aposed
// within single-quoted string
? (aposed = !apos, (aposed ? all : '"'))
: quoted
// within double-quoted string
? (quoted = !quot, (quoted ? all : '"'))
:
(
(lftPrn
? (parenDepth++, pathStart[parenDepth] = index++, lftPrn)
: "")
+ (space
? (parenDepth
? ""
// New arg or prop - so insert backspace \b (\x08) as separator for named params, used subsequently by rBuildHash, and prepare new bindings array
: (paramIndex = full.slice(paramIndex, index), named
? (named = boundName = bindto = false, "\b")
: "\b,") + paramIndex + (paramIndex = index + all.length, bindings && pathBindings.push(bindings = []), "\b")
)
: eq
// named param. Remove bindings for arg and create instead bindings array for prop
? (parenDepth && syntaxError(params), bindings && pathBindings.pop(), named = path, boundName = bound, paramIndex = index + all.length, bound && (bindings = pathBindings[named] = []), path + ':')
: path
// path
? (path.split("^").join(".").replace(rPath, parsePath)
+ (prn
? (fnCall[++parenDepth] = true, path.charAt(0) !== "." && (pathStart[parenDepth] = index), isFn ? "" : 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, bindto, boundName,
quoted, // boolean for string content in double quotes
aposed, // or in single quotes
bindings = pathBindings && pathBindings[0], // bindings array for the first arg
paramIndex = 0, // list,
tmplLinks = tmpl ? tmpl.links : bindings && (bindings.links = bindings.links || {}),
fnCall = {},
pathStart = {0: -1},
parenDepth = 0;
//pushBindin