var htmlUtil = require('html-util')
, md5 = require('MD5')
, parseHtml = htmlUtil.parse
, trimLeading = htmlUtil.trimLeading
, unescapeEntities = htmlUtil.unescapeEntities
, escapeHtml = htmlUtil.escapeHtml
, escapeAttribute = htmlUtil.escapeAttribute
, isVoid = htmlUtil.isVoid
, conditionalComment = htmlUtil.conditionalComment
, lookup = require('racer/lib/path').lookup
, markup = require('./markup')
, viewPath = require('./viewPath')
, patchCtx = viewPath.patchCtx
, wrapRemainder = viewPath.wrapRemainder
, ctxPath = viewPath.ctxPath
, extractPlaceholder = viewPath.extractPlaceholder
, dataValue = viewPath.dataValue
, pathFnArgs = viewPath.pathFnArgs
, isBound = viewPath.isBound
, eventBinding = require('./eventBinding')
, splitEvents = eventBinding.splitEvents
, fnListener = eventBinding.fnListener
, racer = require('racer')
, merge = racer.util.merge
module.exports = View;
function empty() {
return '';
}
var defaultCtx = {
$aliases: {}
, $paths: []
, $indices: []
};
var CAMEL_REGEXP = /([a-z])([A-Z])/g
var defaultGetFns = {
equal: function(a, b) {
return a === b;
}
, not: function(value) {
return !value;
}
, dash: function(value) {
return value && value
.replace(/[:_\s]/g, '-')
.replace(CAMEL_REGEXP, '$1-$2')
.toLowerCase()
}
, join: function(items, property, separator) {
var list, i;
if (!items) return;
if (property) {
list = [];
for (i = items.length; i--;) {
list[i] = items[i][property];
}
} else {
list = items;
}
return list.join(separator || ', ');
}
, log: function(value) {
return console.log.apply(console, arguments);
}
, path: function(name) {
return ctxPath(this.view, this.ctx, name);
}
};
var defaultSetFns = {
equal: function(value, a) {
return value && {1: a};
}
, not: function(value) {
return {0: !value};
}
};
function View(libraries, app, appFilename) {
this._libraries = libraries || (libraries = []);
this.app = app || {};
this._appFilename = appFilename;
this._inline = '';
this.clear();
this.getFns = Object.create(defaultGetFns);
this.setFns = Object.create(defaultSetFns);
if (this._init) this._init();
}
View.prototype = {
defaultViews: {
doctype: function() {
return '';
}
, root: empty
, charset: function() {
return '';
}
, title$s: empty
, head: empty
, header: empty
, body: empty
, footer: empty
, scripts: empty
, tail: empty
}
, _selfNs: 'app'
// All automatically created ids start with a dollar sign
// TODO: change this since it messes up query selectors unless escaped
, _uniqueId: uniqueId
, clear: clear
, _resetForRender: resetForRender
, make: make
, _makeAll: makeAll
, _makeComponents: makeComponents
, _findView: findView
, _find: find
, get: get
, fn: fn
, render: render
, componentsByName: componentsByName
, _componentConstructor: componentConstructor
, _beforeRender: beforeRender
, _afterRender: afterRender
, _beforeRoute: beforeRoute
// TODO: This API is temporary until subscriptions can be properly cleaned up
, whitelistCollections: whitelistCollections
, inline: empty
, escapeHtml: escapeHtml
, escapeAttribute: escapeAttribute
}
View.valueBinding = valueBinding;
function clear() {
this._views = Object.create(this.defaultViews);
this._renders = {};
this._resetForRender();
}
function resetForRender(model, componentInstances) {
componentInstances || (componentInstances = {});
if (model) this.model = model;
this._idCount = 0;
this._componentInstances = componentInstances;
var libraries = this._libraries
, i
for (i = libraries.length; i--;) {
libraries[i].view._resetForRender(model, componentInstances);
}
}
function componentsByName(name) {
return this._componentInstances[name] || [];
}
function componentConstructor(name) {
return this._selfLibrary && this._selfLibrary.constructors[name];
}
function uniqueId() {
return '$' + (this._idCount++).toString(36);
}
function make(name, template, options, templatePath) {
var view = this
, isString = options && options.literal
, noMinify = isString
, onBind, renderer, render, matchTitle;
if (templatePath && (render = this._renders[templatePath])) {
this._views[name] = render;
return
}
name = name.toLowerCase();
matchTitle = /(?:^|\:)title(\$s)?$/.exec(name);
if (matchTitle) {
isString = !!matchTitle[1];
if (isString) {
onBind = function(events, name) {
return bindEvents(events, name, render, ['$_doc', 'prop', 'title']);
};
} else {
this.make(name + '$s', template, options, templatePath);
}
}
renderer = function(ctx, model, triggerPath, triggerId) {
renderer = parse(view, name, template, isString, onBind, noMinify);
return renderer(ctx, model, triggerPath, triggerId);
}
render = function(ctx, model, triggerPath, triggerId) {
return renderer(ctx, model, triggerPath, triggerId);
}
render.nonvoid = options && options.nonvoid;
this._views[name] = render;
if (templatePath) this._renders[templatePath] = render;
}
function makeAll(templates, instances) {
var name, instance, options, templatePath;
if (!instances) return;
this.clear();
for (name in instances) {
instance = instances[name];
templatePath = instance[0];
options = instance[1];
this.make(name, templates[templatePath], options, templatePath);
}
}
function makeComponents(components) {
var librariesMap = this._libraries.map
, name, component, library;
for (name in components) {
component = components[name];
library = librariesMap[name];
library && library.view._makeAll(component.templates, component.instances);
}
}
function findView(name, ns) {
var items = this._views
, item, i, segments, testNs;
name = name.toLowerCase();
if (ns) {
ns = ns.toLowerCase();
item = items[ns + ':' + name];
if (item) return item;
segments = ns.split(':');
for (i = segments.length; i-- > 1;) {
testNs = segments.slice(0, i).join(':');
item = items[testNs + ':' + name];
if (item) return item;
}
}
return items[name];
}
function find(name, ns, optional) {
var view = this._findView(name, ns);
if (view) return view;
if (optional) return empty;
if (ns) name = ns + ':' + name;
throw new Error("Can't find template: \n " + name + '\n\n' +
'Available templates: \n ' + Object.keys(this._views).join('\n ')
);
}
function get(name, ns, ctx) {
if (typeof ns === 'object') {
ctx = ns;
ns = '';
}
ctx = ctx ? extend(ctx, defaultCtx) : Object.create(defaultCtx);
var app = Object.create(this.app, {model: {value: this.model}});
ctx.$fnCtx = [app];
return this._find(name, ns)(ctx);
}
function fn(name, value) {
if (typeof name === 'object') {
for (var k in name) {
this.fn(k, name[k]);
}
return;
}
var get, set;
if (typeof value === 'object') {
get = value.get;
set = value.set;
} else {
get = value;
}
this.getFns[name] = get;
if (set) this.setFns[name] = set;
}
function emitRender(view, ns, ctx, name) {
if (view.isServer) return;
view.app.emit(name, ctx);
if (ns) view.app.emit(name + ':' + ns, ctx);
}
function beforeRender(model, ns, ctx) {
ctx = (ctx && Object.create(ctx)) || {};
ctx.$ns = ns;
ctx.$isProduction = model.flags.isProduction;
emitRender(this, ns, ctx, 'pre:render');
return ctx;
}
function afterRender(ns, ctx) {
this.app.dom._preventUpdates = false;
this.app.dom._emitUpdate();
emitRender(this, ns, ctx, 'render');
}
function beforeRoute() {
this.app.dom._preventUpdates = true;
this.app.dom.clear();
resetModel(this.model, this._collectionWhitelist);
var lastRender = this._lastRender;
if (!lastRender) return;
emitRender(this, lastRender.ns, lastRender.ctx, 'replace');
}
// TODO: This is a super big hack. Subscriptions should automatically clean up.
// When called with an array of collection names, data not in a whitelisted collection
// or a query to a whitelisted collection will be wiped before every route
function whitelistCollections(names) {
if (!names) delete this._collectionWhitelist;
var whitelist = {'_$queries': true}
, i
for (i = names.length; i--;) {
whitelist[names[i]] = true;
}
this._collectionWhitelist = whitelist;
}
function resetModel(model, collectionWhitelist) {
if (collectionWhitelist) {
var world = model._memory._data.world
, queries = world._$queries
, key, collection
var subs = model._subs();
model.unsubscribe.apply(model, subs);
for (key in world) {
if (collectionWhitelist[key]) continue;
delete world[key];
}
for (key in queries) {
collection = queries[key] && queries[key].ns
if (collectionWhitelist[collection]) continue;
delete queries[key];
}
model._specCache.invalidate();
model.emit('removeModelListeners');
}
model.emit('cleanup');
}
function render(model, ns, ctx, renderHash) {
if (typeof ns === 'object') {
rendered = ctx;
ctx = ns;
ns = '';
}
this.model = model;
if (!renderHash) ctx = this._beforeRender(model, ns, ctx);
this._lastRender = {
ns: ns
, ctx: ctx
};
this._resetForRender();
model.__pathMap.clear();
model.__events.clear();
model.__blockPaths = {};
model.silent().del('_$component');
var title = this.get('title$s', ns, ctx)
, rootHtml = this.get('root', ns, ctx)
, bodyHtml = this.get('header', ns, ctx) +
this.get('body', ns, ctx) +
this.get('footer', ns, ctx)
, doc = window.document
, err
if (!model.flags.isProduction && renderHash) {
// Check hashes in development to help find rendering bugs
if (renderHash === md5(bodyHtml)) return;
err = new Error('Server and client page renders do not match');
setTimeout(function() {
throw err;
}, 0);
} else if (renderHash) {
// Don't finish rendering client side on the very first load
return;
}
var documentElement = doc.documentElement
, attrs = documentElement.attributes
, i, attr, fakeRoot, body;
// Remove all current attributes on the documentElement and replace
// them with the attributes in the rendered rootHtml
for (i = attrs.length; i--;) {
attr = attrs[i];
documentElement.removeAttribute(attr.name);
}
// Using the DOM to get the attributes on an tag would require
// some sort of iframe hack until DOMParser has better browser support.
// String parsing the html should be simpler and more efficient
parseHtml(rootHtml, {
start: function(tag, tagName, attrs) {
if (tagName !== 'html') return;
for (var attr in attrs) {
documentElement.setAttribute(attr, attrs[attr]);
}
}
});
fakeRoot = doc.createElement('html');
fakeRoot.innerHTML = bodyHtml;
body = fakeRoot.getElementsByTagName('body')[0];
documentElement.replaceChild(body, doc.body);
doc.title = title;
this._afterRender(ns, ctx);
}
function extend(parent, obj) {
var out = Object.create(parent)
, key;
if (typeof obj !== 'object' || Array.isArray(obj)) {
return out;
}
for (key in obj) {
out[key] = obj[key];
}
return out;
}
function modelListener(params, triggerId, blockPaths, pathId, partial, ctx) {
var listener = typeof params === 'function'
? params(triggerId, blockPaths, pathId)
: params;
listener.partial = partial;
listener.ctx = ctx.$stringCtx || ctx;
return listener;
}
function bindEvents(events, name, partial, params) {
if (~name.indexOf('(')) {
var args = pathFnArgs(name);
if (!args.length) return;
events.push(function(ctx, modelEvents, dom, pathMap, view, blockPaths, triggerId) {
var listener = modelListener(params, triggerId, blockPaths, null, partial, ctx)
, path, pathId, i;
listener.getValue = function(model, triggerPath) {
patchCtx(ctx, triggerPath);
return dataValue(view, ctx, model, name);
}
for (i = args.length; i--;) {
path = ctxPath(view, ctx, args[i]);
pathId = pathMap.id(path + '*');
modelEvents.ids[path] = listener[0];
modelEvents.bind(pathId, listener);
}
});
return;
}
var match = /(\.*)(.*)/.exec(name)
, prefix = match[1] || ''
, relativeName = match[2] || ''
, segments = relativeName.split('.')
, bindName, i;
for (i = segments.length; i; i--) {
bindName = prefix + segments.slice(0, i).join('.');
(function(bindName) {
events.push(function(ctx, modelEvents, dom, pathMap, view, blockPaths, triggerId) {
var path = ctxPath(view, ctx, name)
, listener, pathId;
if (!path) return;
pathId = pathMap.id(path);
listener = modelListener(params, triggerId, blockPaths, pathId, partial, ctx);
if (name !== bindName) {
path = ctxPath(view, ctx, bindName);
pathId = pathMap.id(path);
listener.getValue = function(model, triggerPath) {
patchCtx(ctx, triggerPath);
return dataValue(view, ctx, model, name);
};
}
modelEvents.ids[path] = listener[0];
modelEvents.bind(pathId, listener);
});
})(bindName);
}
}
function bindEventsById(events, name, partial, attrs, method, prop, blockType) {
function params(triggerId, blockPaths, pathId) {
var id = attrs._id || attrs.id;
if (blockType && pathId) {
blockPaths[id] = {id: pathId, type: blockType};
}
return [id, method, prop];
}
bindEvents(events, name, partial, params);
}
function bindEventsByIdString(events, name, partial, attrs, method, prop) {
function params(triggerId) {
var id = triggerId || attrs._id || attrs.id;
return [id, method, prop];
}
bindEvents(events, name, partial, params);
}
function addId(view, attrs) {
if (attrs.id == null) {
attrs.id = function() {
return attrs._id = view._uniqueId();
};
}
}
function pushValue(html, i, value, isAttr, isId) {
if (typeof value === 'function') {
var fn = isId ? function(ctx, model) {
var id = value(ctx, model);
html.ids[id] = i + 1;
return id;
} : value;
i = html.push(fn, '') - 1;
} else {
if (isId) html.ids[value] = i + 1;
html[i] += isAttr ? escapeAttribute(value) : value;
}
return i;
}
function reduceStack(stack) {
var html = ['']
, i = 0
, attrs, bool, item, key, value, j, len;
html.ids = {};
for (j = 0, len = stack.length; j < len; j++) {
item = stack[j];
switch (item[0]) {
case 'start':
html[i] += '<' + item[1];
attrs = item[2];
// Make sure that the id attribute is rendered first
if ('id' in attrs) {
html[i] += ' id=';
i = pushValue(html, i, attrs.id, true, true);
}
for (key in attrs) {
if (key === 'id') continue;
value = attrs[key];
if (value != null) {
if (bool = value.bool) {
i = pushValue(html, i, bool);
continue;
}
html[i] += ' ' + key + '=';
i = pushValue(html, i, value, true);
} else {
html[i] += ' ' + key;
}
}
html[i] += '>';
break;
case 'text':
i = pushValue(html, i, item[1]);
break;
case 'end':
html[i] += '' + item[1] + '>';
break;
case 'marker':
html[i] += '';
}
}
return html;
}
function renderer(view, items, events, onRender) {
return function(ctx, model, triggerPath, triggerId) {
patchCtx(ctx, triggerPath);
if (!model) model = view.model; // Needed, since model parameter is optional
var pathMap = model.__pathMap
, modelEvents = model.__events
, blockPaths = model.__blockPaths
, idIndices = items.ids
, dom = global.DERBY && global.DERBY.app.dom
, html = []
, mutated = []
, onMutator, i, len, item, event, pathIds, id, index;
if (onRender) ctx = onRender(ctx);
onMutator = model.on('mutator', function(method, args) {
mutated.push(args[0][0])
});
for (i = 0, len = items.length; i < len; i++) {
item = items[i];
html[i] = (typeof item === 'function') ? item(ctx, model) || '' : item;
}
model.removeListener('mutator', onMutator)
pathIds = modelEvents.ids = {}
for (i = 0; event = events[i++];) {
event(ctx, modelEvents, dom, pathMap, view, blockPaths, triggerId);
}
// This detects when an already rendered bound value was later updated
// while rendering the rest of the template. This can happen when performing
// component initialization code.
// TODO: This requires creating a whole bunch of extra objects every time
// things are rendered. Should probably be refactored in a less hacky manner.
for (i = 0, len = mutated.length; i < len; i++) {
(id = pathIds[mutated[i]]) &&
(index = idIndices[id]) &&
(html[index] = items[index](ctx, model) || '')
}
return html.join('');
}
}
function bindComponentEvent(component, name, listener) {
if (name === 'init' || name === 'create') {
component.once(name, listener.fn);
} else {
// Extra indirection allows listener to overwrite itself after first run
component.on(name, function() {
listener.fn.apply(null, arguments);
});
}
}
function bindComponentEvents(ctx, component, events) {
var view = events.$view
, items = events.$events
, listenerCtx = Object.create(ctx)
, i, item, name, listener
// The fnCtx will include this component, but we want to emit
// on the parent component or app
listenerCtx.$fnCtx = listenerCtx.$fnCtx.slice(0, -1);
for (i = items.length; i--;) {
item = items[i];
name = item[0];
listener = fnListener(view, listenerCtx, item[2]);
bindComponentEvent(component, name, listener);
}
}
function createComponent(view, model, Component, scope, ctx, macroCtx) {
var scoped = model.at(scope)
, marker = ''
, prefix = scope + '.'
, component = new Component(scoped, scope)
, parentFnCtx = model.__fnCtx || ctx.$fnCtx
, silentCtx = Object.create(ctx, {$silent: {value: true}})
, silentModel = model.silent()
, i, key, path, value, instanceName, instances
ctx.$fnCtx = model.__fnCtx = parentFnCtx.concat(component);
for (key in macroCtx) {
value = macroCtx[key];
if (key === 'bind') {
bindComponentEvents(ctx, component, value);
continue;
}
if (value && value.$matchName) {
path = ctxPath(view, ctx, value.$matchName);
if (value.$bound) {
silentModel.ref(prefix + key, path, null, true);
continue;
}
value = dataValue(view, ctx, model, path);
silentModel.set(prefix + key, value);
continue;
}
// TODO: figure out how to render when needed in component script
if (typeof value === 'function') {
try {
value = value(silentCtx, model);
} catch (err) {
continue;
}
}
silentModel.set(prefix + key, value);
}
instanceName = scoped.get('name');
if (instanceName) {
instances = view._componentInstances[instanceName] ||
(view._componentInstances[instanceName] = []);
instances.push(component);
}
if (component.init) component.init(scoped);
var parent = true
, fnCtx, type
for (i = parentFnCtx.length; fnCtx = parentFnCtx[--i];) {
type = Component.type(fnCtx.view);
if (parent) {
parent = false;
fnCtx.emit('init:child', component, type);
}
fnCtx.emit('init:descendant', component, type);
}
component.emit('init', component);
if (view.isServer || ctx.$silent) return marker;
var app = global.DERBY && global.DERBY.app
, dom = app.dom
component.dom = dom;
component.history = app.history;
dom.nextUpdate(function() {
// Correct for when components get created multiple times
// during rendering
if (!dom.marker(scope)) return component.emit('destroy');
dom.addComponent(ctx, component);
if (component.create) component.create(scoped, component.dom);
var parent = true
, fnCtx, type
for (i = parentFnCtx.length; fnCtx = parentFnCtx[--i];) {
type = Component.type(fnCtx.view);
if (parent) {
parent = false;
fnCtx.emit('create:child', component, type);
}
fnCtx.emit('create:descendant', component, type);
}
component.emit('create', component);
});
return marker;
}
function extendCtx(view, ctx, value, name, alias, index, isArray) {
var path = ctxPath(view, ctx, name, true)
, aliases;
ctx = extend(ctx, value);
ctx['this'] = value;
if (alias) {
aliases = ctx.$aliases = Object.create(ctx.$aliases);
aliases[alias] = ctx.$paths.length;
}
if (path) {
ctx.$paths = [[path, ctx.$indices.length]].concat(ctx.$paths);
}
if (index != null) {
ctx.$indices = [index].concat(ctx.$indices);
ctx.$index = index;
isArray = true;
}
if (isArray && ctx.$paths[0][0]) {
ctx.$paths[0][0] += '.$#';
}
return ctx;
}
function partialValue(view, ctx, model, name, value, listener) {
if (listener) return value;
return name ? dataValue(view, ctx, model, name) : true;
}
function partialFn(view, name, type, alias, render, ns, macroCtx) {
function partialBlock (ctx, model, triggerPath, triggerId, value, index, listener) {
// Inherit & render attribute context values
var renderMacroCtx = {}
, parentMacroCtx = ctx.$macroCtx
, mergedMacroCtx = macroCtx
, key, val, matchName
if (macroCtx.inherit) {
mergedMacroCtx = {};
merge(mergedMacroCtx, parentMacroCtx);
merge(mergedMacroCtx, macroCtx);
delete mergedMacroCtx.inherit;
}
for (key in mergedMacroCtx) {
val = mergedMacroCtx[key];
if (val && val.$matchName) {
matchName = ctxPath(view, ctx, val.$matchName);
if (matchName.charAt(0) === '@') {
val = dataValue(view, ctx, model, matchName);
} else {
val = Object.create(val);
val.$matchName = matchName;
}
}
renderMacroCtx[key] = val;
}
// Find the appropriate partial template
var partialNs, partialName, partialOptional, arr;
if (name === 'derby:view') {
partialNs = mergedMacroCtx.ns || view._selfNs;
partialName = mergedMacroCtx.view;
partialOptional = mergedMacroCtx.optional;
if (!partialName) throw new Error(' tag without a "view" attribute')
if (partialNs.$matchName) {
partialNs = dataValue(view, ctx, model, partialNs.$matchName);
}
if (partialName.$matchName) {
partialName = dataValue(view, ctx, model, partialName.$matchName);
}
} else {
arr = splitPartial(name);
partialNs = arr[0];
partialName = arr[1];
}
// This can happen when using
if (typeof partialName === 'function') {
partialName = partialName(Object.create(ctx), model, triggerPath);
}
var partialView = nsView(view, partialNs)
, render = partialView._find(partialName, ns, partialOptional)
, Component = partialView._componentConstructor(partialName)
, renderCtx, scope, out, marker
// Prepare the context for rendering
if (Component) {
scope = '_$component.' + view._uniqueId();
renderCtx = extendCtx(view, ctx, null, scope, 'self', null, false);
renderCtx.$elements = {};
marker = createComponent(view, model, Component, scope, renderCtx, renderMacroCtx);
} else {
renderCtx = Object.create(ctx);
}
renderCtx.$macroCtx = renderMacroCtx;
out = render(renderCtx, model, triggerPath);
if (Component) {
model.__fnCtx = model.__fnCtx.slice(0, -1);
out = marker + out;
}
return out;
}
function withBlock(ctx, model, triggerPath, triggerId, value, index, listener) {
value = partialValue(view, ctx, model, name, value, listener);
return conditionalRender(ctx, model, triggerPath, value, index, true);
}
function ifBlock(ctx, model, triggerPath, triggerId, value, index, listener) {
value = partialValue(view, ctx, model, name, value, listener);
var condition = !!(Array.isArray(value) ? value.length : value);
return conditionalRender(ctx, model, triggerPath, value, index, condition);
}
function unlessBlock(ctx, model, triggerPath, triggerId, value, index, listener) {
value = partialValue(view, ctx, model, name, value, listener);
var condition = !(Array.isArray(value) ? value.length : value);
return conditionalRender(ctx, model, triggerPath, value, index, condition);
}
function eachBlock(ctx, model, triggerPath, triggerId, value, index, listener) {
var indices, isArray, item, out, renderCtx, i, len;
value = partialValue(view, ctx, model, name, value, listener);
isArray = Array.isArray(value);
if (listener && !isArray) {
return withBlock (ctx, model, triggerPath, triggerId, value, index, true);
}
if (!isArray || !value.length) return;
ctx = extendCtx(view, ctx, null, name, alias, null, true);
out = '';
indices = ctx.$indices;
for (i = 0, len = value.length; i < len; i++) {
item = value[i];
renderCtx = extend(ctx, item);
renderCtx['this'] = item;
renderCtx.$indices = [i].concat(indices);
renderCtx.$index = i;
out += render(renderCtx, model, triggerPath);
}
return out;
}
function conditionalRender(ctx, model, triggerPath, value, index, condition) {
if (!condition) return;
var renderCtx = extendCtx(view, ctx, value, name, alias, index, false);
return render(renderCtx, model, triggerPath);
}
var block =
(type === 'partial') ? partialBlock
: (type === 'with' || type === 'else') ? withBlock
: (type === 'if' || type === 'else if') ? ifBlock
: (type === 'unless') ? unlessBlock
: (type === 'each') ? eachBlock
: null
if (!block) throw new Error('Unknown block type: ' + type);
block.type = type;
return block;
}
var objectToString = Object.prototype.toString;
var arrayToString = Array.prototype.toString;
function valueBinding(value) {
return value == null ? '' :
(value.toString === objectToString || value.toString === arrayToString) ?
JSON.stringify(value) : value;
}
function valueText(value) {
return valueBinding(value).toString();
}
function textFn(view, name, escape, force) {
var filter = escape ? function(value) {
return escape(valueText(value));
} : valueText;
return function(ctx, model) {
return dataValue(view, ctx, model, name, filter, force);
}
}
function sectionFn(view, queue) {
var render = renderer(view, reduceStack(queue.stack), queue.events)
, block = queue.block
, type = block.type
, out = partialFn(view, block.name, type, block.alias, render)
return out;
}
function blockFn(view, sections) {
var len = sections.length;
if (!len) return;
if (len === 1) {
return sectionFn(view, sections[0]);
} else {
var fns = []
, i, out;
for (i = 0; i < len; i++) {
fns.push(sectionFn(view, sections[i]));
}
out = function(ctx, model, triggerPath, triggerId, value, index, listener) {
var out;
for (i = 0; i < len; i++) {
out = fns[i](ctx, model, triggerPath, triggerId, value, index, listener);
if (out != null) return out;
}
}
return out;
}
}
function parseMarkup(type, attr, tagName, events, attrs, value) {
var parser = markup[type][attr]
, anyOut, anyParser, elOut, elParser, out;
if (!parser) return;
if (anyParser = parser['*']) {
anyOut = anyParser(events, attrs, value);
}
if (elParser = parser[tagName]) {
elOut = elParser(events, attrs, value);
}
out = anyOut ? extend(anyOut, elOut) : elOut;
if (out && out.del) delete attrs[attr];
return out;
}
function pushText(stack, text) {
if (text) stack.push(['text', text]);
}
function pushVarFn(view, stack, fn, name, escapeFn) {
if (fn) {
pushText(stack, fn);
} else {
pushText(stack, textFn(view, name, escapeFn));
}
}
function isPartial(view, tagName) {
if (tagName === 'derby:view') return true;
var tagNs = splitPartial(tagName)[0];
return (tagNs === view._selfNs || !!view._libraries.map[tagNs]);
}
function isPartialSection(tagName) {
return tagName.charAt(0) === '@';
}
function partialSectionName(tagName) {
return isPartialSection(tagName) ? tagName.slice(1) : null;
}
function nsView(view, ns) {
if (ns === view._selfNs) return view;
var partialView = view._libraries.map[ns].view;
partialView._uniqueId = function() {
return view._uniqueId();
};
partialView.model = view.model;
return partialView;
}
function splitPartial(partial) {
var i = partial.indexOf(':')
, partialNs = partial.slice(0, i)
, partialName = partial.slice(i + 1)
return [partialNs, partialName];
}
function findComponent(view, partial, ns) {
var arr = splitPartial(partial)
, partialNs = arr[0]
, partialName = arr[1]
, partialView = nsView(view, partialNs)
return partialView._find(partialName, ns);
}
function isVoidComponent(view, partial, ns) {
if (partial === 'derby:view') return true;
return !findComponent(view, partial, ns).nonvoid;
}
function pushVar(view, ns, stack, events, remainder, match, fn) {
var name = match.name
, partial = match.partial
, escapeFn = match.escaped && escapeHtml
, attr, attrs, boundOut, last, tagName, wrap;
if (partial) {
fn = partialFn(view, partial, 'partial', null, null, ns, match.macroCtx);
}
else if (match.bound) {
last = lastItem(stack);
wrap = match.pre ||
!last ||
(last[0] !== 'start') ||
isVoid(tagName = last[1]) ||
wrapRemainder(tagName, remainder);
if (wrap) {
stack.push(['marker', '', attrs = {}]);
} else {
attrs = last[2];
for (attr in attrs) {
parseMarkup('boundParent', attr, tagName, events, attrs, match);
}
boundOut = parseMarkup('boundParent', '*', tagName, events, attrs, match);
if (boundOut) {
bindEventsById(events, name, null, attrs, boundOut.method, boundOut.property);
}
}
addId(view, attrs);
if (!boundOut) {
bindEventsById(events, name, fn, attrs, 'html', !fn && escapeFn, match.type);
}
}
pushVarFn(view, stack, fn, name, escapeFn);
if (wrap) {
stack.push([
'marker'
, '$'
, { id: function() { return attrs._id } }
]);
}
}
function pushVarString(view, ns, stack, events, remainder, match, fn) {
var name = match.name
, escapeFn = !match.escaped && unescapeEntities;
function bindOnce(ctx) {
ctx.$onBind(events, name);
bindOnce = empty;
}
if (match.bound) {
events.push(function(ctx) {
bindOnce(ctx);
});
}
pushVarFn(view, stack, fn, name, escapeFn);
}
function parseMatchError(text, message) {
throw new Error(message + '\n\n' + text + '\n');
}
function onBlock(start, end, block, queues, callbacks) {
var lastQueue, queue;
if (end) {
lastQueue = queues.pop();
queue = lastItem(queues);
queue.sections.push(lastQueue);
} else {
queue = lastItem(queues);
}
if (start) {
queue = {
stack: []
, events: []
, block: block
, sections: []
};
queues.push(queue);
callbacks.onStart(queue);
} else {
if (end) {
callbacks.onStart(queue);
callbacks.onEnd(queue.sections);
queue.sections = [];
} else {
callbacks.onContent(block);
}
}
}
function parseMatch(text, match, queues, callbacks) {
var hash = match.hash
, type = match.type
, name = match.name
, block = lastItem(queues).block
, blockType = block && block.type
, startBlock, endBlock;
if (type === 'if' || type === 'unless' || type === 'each' || type === 'with') {
if (hash === '#') {
startBlock = true;
} else if (hash === '/') {
endBlock = true;
} else {
parseMatchError(text, type + ' blocks must begin with a #');
}
} else if (type === 'else' || type === 'else if') {
if (hash) {
parseMatchError(text, type + ' blocks may not start with ' + hash);
}
if (blockType !== 'if' && blockType !== 'else if' &&
blockType !== 'unless' && blockType !== 'each') {
parseMatchError(text, type + ' may only follow `if`, `else if`, `unless`, or `each`');
}
startBlock = true;
endBlock = true;
} else if (hash === '/') {
endBlock = true;
} else if (hash === '#') {
parseMatchError(text, '# must be followed by `if`, `unless`, `each`, or `with`');
}
if (endBlock && !block) {
parseMatchError(text, 'Unmatched template end tag');
}
onBlock(startBlock, endBlock, match, queues, callbacks);
}
function parseAttr(view, viewName, events, tagName, attrs, attr) {
var value = attrs[attr];
if (typeof value === 'function') return;
var attrOut = parseMarkup('attr', attr, tagName, events, attrs, value) || {}
, boundOut, match, name, render, method, property;
if (attrOut.addId) addId(view, attrs);
if (match = extractPlaceholder(value)) {
name = match.name;
if (match.pre || match.post) {
// Attributes must be a single string, so create a string partial
addId(view, attrs);
render = parse(view, viewName, value, true, function(events, name) {
bindEventsByIdString(events, name, render, attrs, 'attr', attr);
});
attrs[attr] = attr === 'id' ? function(ctx, model) {
return attrs._id = escapeAttribute(render(ctx, model));
} : function(ctx, model) {
return escapeAttribute(render(ctx, model));
}
return;
}
if (match.bound) {
boundOut = parseMarkup('bound', attr, tagName, events, attrs, match) || {};
addId(view, attrs);
method = boundOut.method || 'attr';
property = boundOut.property || attr;
bindEventsById(events, name, null, attrs, method, property);
}
if (!attrOut.del) {
attrs[attr] = attrOut.bool ? {
bool: function(ctx, model) {
return (dataValue(view, ctx, model, name)) ? ' ' + attr : '';
}
} : textFn(view, name, escapeAttribute, true);
}
}
}
function parsePartialAttr(view, viewName, events, attrs, attr) {
var value = attrs[attr]
, match;
if (!value) {
// A true boolean attribute will have a value of null
if (value === null) attrs[attr] = true;
return;
}
if (attr === 'bind') {
attrs[attr] = {$events: splitEvents(value), $view: view};
return;
}
if (match = extractPlaceholder(value)) {
// This attribute needs to be treated as a section
if (match.pre || match.post) return true;
attrs[attr] = {$matchName: match.name, $bound: match.bound};
} else if (value === 'true') {
attrs[attr] = true;
} else if (value === 'false') {
attrs[attr] = false;
} else if (value === 'null') {
attrs[attr] = null;
} else if (!isNaN(value)) {
attrs[attr] = +value;
} else if (/^[{[]/.test(value)) {
try {
attrs[attr] = JSON.parse(value)
} catch (err) {}
}
}
function lastItem(arr) {
return arr[arr.length - 1];
}
function parse(view, viewName, template, isString, onBind, noMinify) {
var queues, stack, events, onRender, push;
queues = [{
stack: stack = []
, events: events = []
, sections: []
}];
function onStart(queue) {
stack = queue.stack;
events = queue.events;
}
if (isString) {
push = pushVarString;
onRender = function(ctx) {
if (ctx.$stringCtx) return ctx;
ctx = Object.create(ctx);
ctx.$onBind = onBind;
ctx.$stringCtx = ctx;
return ctx;
}
} else {
push = pushVar;
}
var index = viewName.lastIndexOf(':')
, ns = ~index ? viewName.slice(0, index) : ''
function parseStart(tag, tagName, attrs) {
var attr, block, out, parser, isSection, attrBlock
if ('x-no-minify' in attrs) {
delete attrs['x-no-minify'];
noMinify = true;
}
if (isPartial(view, tagName)) {
block = {
partial: tagName
, macroCtx: attrs
};
onBlock(true, false, block, queues, {onStart: onStart});
for (attr in attrs) {
isSection = parsePartialAttr(view, viewName, events, attrs, attr);
if (!isSection) continue;
attrBlock = {
partial: '@' + attr
, macroCtx: lastItem(queues).block.macroCtx
};
onBlock(true, false, attrBlock, queues, {onStart: onStart});
parseText(attrs[attr]);
parseEnd(tag, '@' + attr);
}
if (isVoidComponent(view, tagName, ns)) {
onBlock(false, true, null, queues, {
onStart: onStart
, onEnd: function(queues) {
push(view, ns, stack, events, '', block);
}
})
}
return;
}
if (isPartialSection(tagName)) {
block = {
partial: tagName
, macroCtx: lastItem(queues).block.macroCtx
};
onBlock(true, false, block, queues, {onStart: onStart});
return;
}
if (parser = markup.element[tagName]) {
out = parser(events, attrs);
if (out != null ? out.addId : void 0) {
addId(view, attrs);
}
}
for (attr in attrs) {
parseAttr(view, viewName, events, tagName, attrs, attr);
}
stack.push(['start', tagName, attrs]);
}
function parseText(text, isRawText, remainder) {
var match = extractPlaceholder(text)
, post, pre;
if (!match || isRawText) {
if (!noMinify) {
text = isString ? unescapeEntities(trimLeading(text)) : trimLeading(text);
}
pushText(stack, text);
return;
}
pre = match.pre;
post = match.post;
if (isString) pre = unescapeEntities(pre);
pushText(stack, pre);
remainder = post || remainder;
parseMatch(text, match, queues, {
onStart: onStart
, onEnd: function(sections) {
var fn = blockFn(view, sections);
push(view, ns, stack, events, remainder, sections[0].block, fn);
}
, onContent: function(match) {
push(view, ns, stack, events, remainder, match);
}
});
if (post) return parseText(post);
}
function parseEnd(tag, tagName) {
var sectionName = partialSectionName(tagName)
, endsPartial = isPartial(view, tagName)
if (endsPartial && isVoidComponent(view, tagName, ns)) {
throw new Error('End tag "' + tag + '" is not allowed for void component')
}
if (sectionName || endsPartial) {
onBlock(false, true, null, queues, {
onStart: onStart
, onEnd: function(queues) {
var queue = queues[0]
, block = queue.block
, fn = renderer(view, reduceStack(queue.stack), queue.events)
fn.unescaped = true;
if (sectionName) {
block.macroCtx[sectionName] = fn;
return;
}
// Put the remaining content not in a section in the default "content" section,
// unless "inherit" is specified and there is no content, so that the parent
// content can be inherited
if (queue.stack.length || !block.macroCtx.inherit) {
block.macroCtx.content = fn;
}
push(view, ns, stack, events, '', block);
}
})
return;
}
stack.push(['end', tagName]);
}
if (isString) {
parseText(template);
} else {
parseHtml(template, {
start: parseStart
, text: parseText
, end: parseEnd
, comment: function(tag) {
if (conditionalComment(tag)) pushText(stack, tag);
}
, other: function(tag) {
pushText(stack, tag);
}
});
}
return renderer(view, reduceStack(stack), events, onRender);
}