derby
Version:
MVC framework making it easy to write realtime, collaborative applications that run in both Node.js and browsers.
1,316 lines (1,153 loc) • 36.4 kB
JavaScript
var htmlUtil = require('html-util')
, 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')
, wrapRemainder = viewPath.wrapRemainder
, ctxPath = viewPath.ctxPath
, extractPlaceholder = viewPath.extractPlaceholder
, dataValue = viewPath.dataValue
, pathFnArgs = viewPath.pathFnArgs
, isBound = viewPath.isBound
module.exports = View;
function empty() {
return '';
}
function notFound(name, ns) {
if (ns) name = ns + ':' + name;
throw new Error("Can't find view: " + name);
}
var defaultCtx = {
$aliases: {}
, $paths: []
, $indices: []
};
var defaultGetFns = {
equal: function(a, b) {
return a === b;
}
, not: function(value) {
return !value;
}
, 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() {
console.log.apply(console, arguments);
}
, path: function(name, macro) {
return ctxPath(this.view, this.ctx, name, macro);
}
};
var defaultSetFns = {
equal: function(value, a) {
return value ? [a] : [];
}
, not: function(value) {
return [!value];
}
};
function View(libraries, appExports) {
this._libraries = libraries || [];
this._appExports = appExports;
this._inline = '';
this.clear();
this.getFns = Object.create(defaultGetFns);
this.setFns = Object.create(defaultSetFns);
}
View.prototype = {
defaultViews: {
doctype: function() {
return '<!DOCTYPE html>';
}
, root: empty
, charset: function() {
return '<meta charset=utf-8>';
}
, 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
, make: make
, _makeAll: makeAll
, _makeComponents: makeComponents
, _findItem: findItem
, _find: find
, get: get
, fn: fn
, render: render
, _afterRender: function(ns, ctx) {
afterRender(this.dom, this._appExports, ns, ctx);
}
, inline: empty
, escapeHtml: escapeHtml
, escapeAttribute: escapeAttribute
, _valueText: valueText
}
function clear() {
this._views = Object.create(this.defaultViews);
this._made = {};
this._renders = {};
this._idCount = 0;
}
function uniqueId() {
return '$' + (this._idCount++).toString(36);
}
function make(name, template, options, templatePath, macroAttrs) {
var view = this
, onBind, renderer, render, matchTitle, ns, isString;
// Cache any templates that are made so that they can be
// re-parsed with different items bound when using macros
this._made[name] = [template, options, templatePath];
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) {
var macro = false;
return bindEvents(events, macro, 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, macroAttrs);
return renderer(ctx, model, triggerPath, triggerId);
}
render = function(ctx, model, triggerPath, triggerId) {
return renderer(ctx, model, triggerPath, triggerId);
}
render.nonvoid = options && 'nonvoid' in options;
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, view;
for (name in components) {
component = components[name];
view = librariesMap[name].view;
view._makeAll(component.templates, component.instances);
}
}
function findItem(name, ns, prop) {
var items = this[prop]
, item, i, segments, testNs;
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, macroAttrs) {
var hash, hashedName, out, item, template, options, templatePath;
if (macroAttrs && (hash = boundHash(macroAttrs))) {
hash = '$b(' + hash + ')';
hashedName = name + hash;
out = this._findItem(hashedName, ns, '_views');
if (out) return out;
item = this._findItem(name, ns, '_made') || notFound(name, ns);
template = item[0];
options = item[1];
templatePath = item[2] + hash;
this.make(hashedName, template, options, templatePath, macroAttrs);
return this._find(hashedName, ns);
}
return this._findItem(name, ns, '_views') || notFound(name, ns);
}
function get(name, ns, ctx) {
if (typeof ns === 'object') {
ctx = ns;
ns = '';
}
ctx = ctx ? extend(ctx, defaultCtx) : Object.create(defaultCtx);
ctx.$fnCtx = [this._appExports];
return this._find(name, ns)(ctx);
}
function fn(name, fn) {
var get, set;
if (typeof fn === 'object') {
get = fn.get;
set = fn.set;
} else {
get = fn;
}
this.getFns[name] = get;
if (set) this.setFns[name] = set;
}
function afterRender(dom, app, ns, ctx) {
dom._emitUpdate();
app.emit('render', ctx);
if (ns) app.emit('render:' + ns, ctx);
}
function render(model, ns, ctx, silent) {
if (typeof ns === 'object') {
silent = ctx;
ctx = ns;
ns = '';
}
this.model = model;
var dom = this.dom
, app = this._appExports
, lastRender = this._lastRender
dom._preventUpdates = true;
if (lastRender) {
app.emit('replace', lastRender.ctx);
if (lastRender.ns) app.emit('replace:' + lastRender.ns, lastRender.ctx);
}
this._lastRender = {
ns: ns
, ctx: ctx
};
this._idCount = 0;
dom.clear();
model.__pathMap.clear();
model.__events.clear();
model.__blockPaths = {};
model.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);
dom._preventUpdates = false;
if (silent) return;
var doc = document
, 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 <html> 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;
afterRender(dom, app, ns, ctx);
}
function boundHash(macroAttrs) {
var keys = []
, key, value;
for (key in macroAttrs) {
value = macroAttrs[key];
if (value && value.$bound) {
keys.push(key);
}
}
return keys.sort().join(',');
}
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, macro, 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, macro);
}
for (i = args.length; i--;) {
path = ctxPath(view, ctx, args[i], macro);
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, macro)
, 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, macro);
pathId = pathMap.id(path);
listener.getValue = function(model, triggerPath) {
patchCtx(ctx, triggerPath);
return dataValue(view, ctx, model, name, macro);
};
}
modelEvents.ids[path] = listener[0];
modelEvents.bind(pathId, listener);
});
})(bindName);
}
}
function bindEventsById(events, macro, name, partial, attrs, method, prop, isBlock) {
function params(triggerId, blockPaths, pathId) {
var id = attrs._id || attrs.id;
if (isBlock && pathId) blockPaths[id] = pathId;
return [id, method, prop];
}
bindEvents(events, macro, name, partial, params);
}
function bindEventsByIdString(events, macro, name, partial, attrs, method, prop) {
function params(triggerId) {
var id = triggerId || attrs._id || attrs.id;
return [id, method, prop];
}
bindEvents(events, macro, 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] += '<!--' + item[1];
i = pushValue(html, i, item[2].id, false, !item[1]);
html[i] += '-->';
}
}
return html;
}
function patchCtx(ctx, triggerPath) {
var meta, path;
if (!(triggerPath && (meta = ctx.$paths[0]) && (path = meta[0]))) return;
var segments = path.split('.')
, triggerSegments = triggerPath.replace(/\*$/, '').split('.')
, indices = ctx.$indices.slice()
, index = indices.length
, i, len, segment, triggerSegment, n;
for (i = 0, len = segments.length; i < len; i++) {
segment = segments[i];
triggerSegment = triggerSegments[i];
// `(n = +triggerSegment) === n` will be false only if segment is NaN
if (segment === '$#' && (n = +triggerSegment) === n) {
indices[--index] = n;
} else if (segment !== triggerSegment) {
break;
}
}
ctx.$indices = indices;
ctx.$index = indices[0];
}
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 = view.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 createComponent(view, model, ns, name, scope, ctx, macroCtx, macroAttrs) {
var library = ns === 'lib' ? view._selfLibrary : view._libraries.map[ns]
, Component = library && library.constructors[name]
if (!Component) return false;
var dom = view.dom
, scoped = model.at(scope)
, prefix = scope + '.'
, component = new Component(scoped)
, parentFnCtx = model.__fnCtx || ctx.$fnCtx
, fnCtx, i, key, path, value, instanceName, parent, type;
ctx.$fnCtx = model.__fnCtx = parentFnCtx.concat(component);
for (key in macroCtx) {
value = macroCtx[key];
if (path = value && value.$matchName) {
path = ctxPath(view, ctx, path);
model.ref(prefix + key, path, null, true);
continue;
}
if (typeof value === 'function') value = value(ctx, model);
model.set(prefix + key, value);
}
instanceName = scoped.get('name');
if (component.init) component.init(scoped);
parent = true;
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);
if (instanceName) {
fnCtx.emit('init:' + instanceName, component, type);
}
}
if (view.isServer) return true;
dom.nextUpdate(function() {
var parent = true
, 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);
if (instanceName) {
fnCtx.emit('create:' + instanceName, component, type);
}
}
});
if (!component.create) return true;
dom.nextUpdate(function() {
var componentDom = component.dom = dom.componentDom(ctx);
// TODO: This is a hack to correct for when components get created
// multiple times during rendering. Should figure out something cleaner
if (!scoped.get()) return;
for (name in ctx.$elements) {
if (!componentDom.element(name)) return;
break;
}
component.create(scoped, componentDom);
});
return true;
}
function extendCtx(view, ctx, value, name, alias, index, isArray, macro) {
var path = ctxPath(view, ctx, name, macro, 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, macro) {
if (listener) return value;
return name ? dataValue(view, ctx, model, name, macro) : true;
}
function partialFn(view, name, type, alias, render, macroCtx, macro, macroAttrs) {
function conditionalRender(ctx, model, triggerPath, value, index, condition) {
if (condition) {
var renderCtx = extendCtx(view, ctx, value, name, alias, index, false, macro);
return render(renderCtx, model, triggerPath);
}
return '';
}
function withFn(ctx, model, triggerPath, triggerId, value, index, listener) {
value = partialValue(view, ctx, model, name, value, listener, macro);
return conditionalRender(ctx, model, triggerPath, value, index, true);
}
if (type === 'partial') {
return function(ctx, model, triggerPath, triggerId, value, index, listener) {
var renderMacroCtx = Object.create(macroCtx)
, parentMacroCtx = ctx.$macroCtx
, renderCtx, key, val, scope, out, hasScope;
for (key in macroCtx) {
val = macroCtx[key];
if (val && val.$macroName) {
val = renderMacroCtx[key] = parentMacroCtx[val.$macroName];
}
if (val && val.$matchName) {
val = renderMacroCtx[key] = Object.create(val)
val.$matchName = ctxPath(view, ctx, val.$matchName)
}
}
if (alias) {
scope = '_$component.' + model.id();
renderCtx = extendCtx(view, ctx, null, scope, alias, null, false, macro);
hasScope = createComponent(view, model, name[0], name[1], scope, renderCtx, renderMacroCtx, macroAttrs);
} else {
renderCtx = Object.create(ctx);
}
renderCtx.$macroCtx = renderMacroCtx;
renderCtx.$elements = {};
out = render(renderCtx, model, triggerPath);
if (hasScope) model.__fnCtx = model.__fnCtx.slice(0, -1);
return out;
}
}
if (type === 'with' || type === 'else') {
return withFn;
}
if (type === 'if' || type === 'else if') {
return function(ctx, model, triggerPath, triggerId, value, index, listener) {
value = partialValue(view, ctx, model, name, value, listener, macro);
var condition = !!(Array.isArray(value) ? value.length : value);
return conditionalRender(ctx, model, triggerPath, value, index, condition);
}
}
if (type === 'unless') {
return function(ctx, model, triggerPath, triggerId, value, index, listener) {
value = partialValue(view, ctx, model, name, value, listener, macro);
var condition = !(Array.isArray(value) ? value.length : value);
return conditionalRender(ctx, model, triggerPath, value, index, condition);
}
}
if (type === 'each') {
return function(ctx, model, triggerPath, triggerId, value, index, listener) {
var indices, isArray, item, out, renderCtx, i, len;
value = partialValue(view, ctx, model, name, value, listener, macro);
isArray = Array.isArray(value);
if (listener && !isArray) {
return withFn(ctx, model, triggerPath, triggerId, value, index, true);
}
if (!isArray) return '';
ctx = extendCtx(view, ctx, null, name, alias, null, true, macro);
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;
}
}
throw new Error('Unknown block type: ' + type);
}
var objectToString = Object.prototype.toString;
var arrayToString = Array.prototype.toString;
function valueText(value) {
return typeof value === 'string' ? value :
value == null ? '' :
(value.toString === objectToString || value.toString === arrayToString) ?
JSON.stringify(value) : value.toString();
}
function textFn(view, name, escape, macro) {
return function(ctx, model) {
var value = dataValue(view, ctx, model, name, macro)
, text = valueText(value);
// TODO: DRY. This is duplicating logic in dataValue()
if (macro) {
value = lookup(name.toLowerCase(), ctx.$macroCtx);
if (typeof value === 'function' && value.unescaped) {
return text;
}
}
return escape ? escape(text) : text;
}
}
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, null, block.macro)
out.type = type;
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, fn;
for (i = 0; i < len; i++) {
fn = fns[i];
out = fn(ctx, model, triggerPath, triggerId, value, index, listener);
if (out) return out;
}
return '';
}
out.type = 'multi';
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, macro) {
if (fn) {
pushText(stack, fn);
} else {
pushText(stack, textFn(view, name, escapeFn, macro));
}
}
function isPartial(view, partial) {
var arr = partial.split(':')
, partialNs = arr[0];
return arr.length >= 2 &&
(partialNs === view._selfNs || !!view._libraries.map[partialNs]);
}
function isPartialSection(tagName) {
return tagName.charAt(0) === '@';
}
function partialSectionName(tagName) {
return isPartialSection(tagName) ? tagName.slice(1) : null;
}
function splitPartial(view, partial, ns) {
var i = partial.indexOf(':')
, partialNs = partial.slice(0, i)
, partialName = partial.slice(i + 1)
, partialView;
if (partialNs !== view._selfNs) {
partialView = view._libraries.map[partialNs].view;
partialView._uniqueId = function() {
return view._uniqueId();
};
partialView.model = view.model;
partialView.dom = view.dom;
} else {
partialView = view;
}
return [partialNs, partialName, partialView];
}
function findComponent(view, partial, ns) {
var arr = splitPartial(view, partial, ns)
, partialName = arr[1]
, view = arr[2];
return view._find(partialName, ns);
}
function isVoidComponent(view, partial, ns) {
return !findComponent(view, partial, ns).nonvoid;
}
function pushVar(view, ns, stack, events, macroAttrs, remainder, match, fn) {
var name = match.name
, partial = match.partial
, macro = match.macro
, escapeFn = match.escaped && escapeHtml
, attr, attrs, boundOut, last, tagName, wrap, render, isBlock;
if (partial) {
var arr = splitPartial(view, partial, ns)
, partialNs = arr[0]
, partialName = arr[1]
, alias = partialNs === 'app' ? '' : 'self'
render = arr[2]._find(partialName, ns, macroAttrs);
fn = partialFn(view, arr, 'partial', alias, render, match.macroCtx, null, macroAttrs);
}
else if (isBound(macroAttrs, match)) {
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, macro, name, null, attrs, boundOut.method, boundOut.property);
}
}
addId(view, attrs);
if (!boundOut) {
isBlock = !!match.type;
bindEventsById(events, macro, name, fn, attrs, 'html', !fn && escapeFn, isBlock);
}
}
pushVarFn(view, stack, fn, name, escapeFn, macro);
if (wrap) {
stack.push([
'marker'
, '$'
, { id: function() { return attrs._id } }
]);
}
}
function pushVarString(view, ns, stack, events, macroAttrs, remainder, match, fn) {
var name = match.name
, escapeFn = !match.escaped && unescapeEntities;
function bindOnce(ctx) {
ctx.$onBind(events, name);
bindOnce = empty;
}
if (isBound(macroAttrs, match)) {
events.push(function(ctx) {
bindOnce(ctx);
});
}
pushVarFn(view, stack, fn, name, escapeFn, match.macro);
}
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: []
, macroAttrs: queue.macroAttrs
};
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, macroAttrs, tagName, attrs, attr) {
var value = attrs[attr];
if (typeof value === 'function') return;
var attrOut = parseMarkup('attr', attr, tagName, events, attrs, value) || {}
, boundOut, macro, match, name, render, method, property;
if (attrOut.addId) addId(view, attrs);
if (match = extractPlaceholder(value)) {
name = match.name;
macro = match.macro;
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, macro, name, render, attrs, 'attr', attr);
}, macroAttrs);
attrs[attr] = attr === 'id' ? function(ctx, model) {
return attrs._id = escapeAttribute(render(ctx, model));
} : function(ctx, model) {
return escapeAttribute(render(ctx, model));
}
return;
}
if (isBound(macroAttrs, match)) {
boundOut = parseMarkup('bound', attr, tagName, events, attrs, match) || {};
addId(view, attrs);
method = boundOut.method || 'attr';
property = boundOut.property || attr;
bindEventsById(events, macro, name, null, attrs, method, property);
}
if (!attrOut.del) {
macro = match.macro;
attrs[attr] = attrOut.bool ? {
bool: function(ctx, model) {
return (dataValue(view, ctx, model, name, macro)) ? ' ' + attr : '';
}
} : textFn(view, name, escapeAttribute, macro);
}
}
}
function parsePartialAttr(view, viewName, events, attrs, attr) {
var value = attrs[attr]
, match, firstChar, lastAttrs, matchName;
if (attr === 'content') {
throw new Error('components may not have an attribute named "content"');
}
if (!value) {
// A true boolean attribute will have a value of null
if (value === null) attrs[attr] = true;
return;
}
if (match = extractPlaceholder(value)) {
if (match.pre || match.post) {
throw new Error('unimplemented: blocks in component attributes');
}
matchName = match.name;
attrs[attr] = match.macro ? {
$macroName: matchName
} : {
$matchName: matchName
, $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, macroAttrs) {
var queues, stack, events, onRender, push;
queues = [{
stack: stack = []
, events: events = []
, sections: []
, macroAttrs: macroAttrs
}];
function onStart(queue) {
stack = queue.stack;
events = queue.events;
macroAttrs = queue.macroAttrs;
}
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) : ''
, minifyContent = true;
function parseStart(tag, tagName, attrs) {
var attr, block, out, parser
if ('x-no-minify' in attrs) {
delete attrs['x-no-minify'];
minifyContent = false;
} else {
minifyContent = true;
}
if (isPartial(view, tagName)) {
block = {
partial: tagName
, macroCtx: attrs
};
onBlock(true, false, block, queues, {onStart: onStart});
lastItem(queues).macroAttrs = macroAttrs = attrs;
for (attr in attrs) {
parsePartialAttr(view, viewName, events, attrs, attr);
}
if (isVoidComponent(view, tagName, ns)) {
onBlock(false, true, null, queues, {
onStart: onStart
, onEnd: function(queues) {
push(view, ns, stack, events, attrs, '', 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, macroAttrs, tagName, attrs, attr);
}
stack.push(['start', tagName, attrs]);
}
function parseText(text, isRawText, remainder) {
var match = extractPlaceholder(text)
, post, pre;
if (!match || isRawText) {
if (minifyContent) {
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, macroAttrs, remainder, sections[0].block, fn);
}
, onContent: function(match) {
push(view, ns, stack, events, macroAttrs, remainder, match);
}
});
if (post) return parseText(post);
}
function parseEnd(tag, tagName) {
var sectionName = partialSectionName(tagName)
, endsPartial = isPartial(view, tagName)
, ctxName = sectionName || endsPartial && 'content'
, attrs = macroAttrs
if (endsPartial && isVoidComponent(view, tagName, ns)) {
throw new Error('End tag "' + tag + '" is not allowed for void component')
}
if (ctxName) {
onBlock(false, true, null, queues, {
onStart: onStart
, onEnd: function(queues) {
var queue = queues[0]
, block = queue.block
// Note that the ctx will be one level too deep, so we use its
// prototype when rendering the section
, fn = renderer(view, reduceStack(queue.stack), queue.events, Object.getPrototypeOf)
fn.unescaped = true;
block.macroCtx[ctxName] = fn;
if (sectionName) return;
push(view, ns, stack, events, attrs, '', 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);
}