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
, markup = require('./markup')
, viewPath = require('./viewPath')
, wrapRemainder = viewPath.wrapRemainder
, ctxPath = viewPath.ctxPath
, extractPlaceholder = viewPath.extractPlaceholder
, dataValue = viewPath.dataValue
, pathFnArgs = viewPath.pathFnArgs
, arraySlice = Array.prototype.slice;
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 = {
$depth: 0
, $aliases: {}
, $paths: []
, $indices: []
};
var defaultGetFns = {
equal: function(a, b) {
return a === b;
}
, not: function(value) {
return !value;
}
};
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._nonvoidComponents = {};
this.clear();
this.getFns = Object.create(defaultGetFns);
this.setFns = Object.create(defaultSetFns);
}
View.prototype = {
clear: function() {
this._views = Object.create(this.defaultViews);
this._made = {};
this._renders = {};
this._inline = '';
this._idCount = 0;
}
// All automatically created ids start with a dollar sign
, _uniqueId: function() {
return '$' + (this._idCount++).toString(36);
}
, 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'
, make: function(name, template, options, templatePath, boundMacro) {
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 (options && 'nonvoid' in options) {
this._nonvoidComponents[name] = true;
}
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, boundMacro);
return renderer(ctx, model, triggerPath, triggerId);
}
render = function(ctx, model, triggerPath, triggerId) {
return renderer(ctx, model, triggerPath, triggerId);
}
this._views[name] = render;
if (templatePath) this._renders[templatePath] = render;
}
, _makeAll: function(templates, instances) {
var name, instance, options, templatePath;
this.clear();
for (name in instances) {
instance = instances[name];
templatePath = instance[0];
options = instance[1];
this.make(name, templates[templatePath], options, templatePath);
}
}
, _makeComponents: function(components) {
var libraries = this._libraries
, name, component, view;
for (name in components) {
component = components[name];
view = libraries[name].view;
view._makeAll(component.templates, component.instances);
}
}
, _findItem: function(name, ns, prop) {
var items = this[prop]
, item, last, i, segments, testNs;
if (ns) {
ns = ns.toLowerCase();
item = items[ns + ':' + name];
if (item) return item;
segments = ns.split(':');
last = segments.length - 1;
if (last > 0) {
for (i = last; i--;) {
testNs = segments.slice(0, i).join(':');
item = items[testNs + ':' + name];
if (item) return item;
}
}
}
return items[name];
}
, _find: function(name, ns, boundMacro) {
var hash, hashedName, out, item, template, options, templatePath;
if (boundMacro && (hash = keyHash(boundMacro))) {
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, boundMacro);
return this._find(hashedName, ns);
}
return this._findItem(name, ns, '_views') || notFound(name, ns);
}
, get: function(name, ns, ctx) {
if (typeof ns === 'object') {
ctx = ns;
ns = '';
}
ctx = ctx ? extend(ctx, defaultCtx) : Object.create(defaultCtx);
return this._find(name, ns)(ctx);
}
, inline: empty
, fn: function(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;
}
, render: function(model, ns, ctx, silent) {
if (typeof ns === 'object') {
silent = ctx;
ctx = ns;
ns = '';
}
this.model = model;
this._idCount = 0;
this.model.__pathMap.clear();
this.model.__events.clear();
this.model.__blockPaths = {};
this.model.del('_$component');
this.dom.clear();
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);
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 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;
}
, escapeHtml: escapeHtml
, escapeAttribute: escapeAttribute
}
function keyHash(obj) {
var keys = []
, key;
for (key in obj) {
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(ctx, args[i], macro);
pathId = pathMap.id(path + '*');
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(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(ctx, bindName, macro);
pathId = pathMap.id(path);
listener.getValue = function(model, triggerPath) {
patchCtx(ctx, triggerPath);
return dataValue(view, ctx, model, name, macro);
};
}
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 reduceStack(stack) {
var html = ['']
, i = 0
, attrs, bool, item, key, value, j, len;
function pushValue(value, isAttr) {
if (value && value.call) {
return i = html.push(value, '') - 1;
} else {
return html[i] += isAttr ? escapeAttribute(value) : value;
}
}
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=';
pushValue(attrs.id, true);
}
for (key in attrs) {
if (key === 'id') continue;
value = attrs[key];
if (value != null) {
if (bool = value.bool) {
pushValue(bool);
continue;
}
html[i] += ' ' + key + '=';
pushValue(value, true);
} else {
html[i] += ' ' + key;
}
}
html[i] += '>';
break;
case 'text':
pushValue(item[1]);
break;
case 'end':
html[i] += '' + item[1] + '>';
break;
case 'marker':
html[i] += '';
}
}
return html;
}
function patchCtx(ctx, triggerPath) {
var path = ctx.$paths[0];
if (!(triggerPath && path)) 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;
}
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
, dom = view.dom
, html = ''
, i, len, item, event;
if (onRender) ctx = onRender(ctx);
for (i = 0, len = items.length; i < len; i++) {
item = items[i];
html += typeof item === 'function' ? item(ctx, model) || '' : item;
}
for (i = 0; event = events[i++];) {
event(ctx, modelEvents, dom, pathMap, view, blockPaths, triggerId);
}
return html;
}
}
function createComponent(view, model, ns, name, scope, ctx, macroCtx, boundMacro) {
var library = view._libraries[ns]
, script = library && library.scripts[name];
if (!script) return;
var initComponent = script.init
, isServer = view.isServer
, key, path, value, prefix, scoped;
if (!initComponent && isServer) return;
ctx.$fnCtx = script;
scoped = model.at(scope);
prefix = scope + '.';
for (key in macroCtx) {
path = boundMacro[key];
if (path) {
path = ctxPath(ctx, path);
model.ref(prefix + key, path, null, true);
continue;
}
value = macroCtx[key];
if (typeof value === 'function') continue;
model.set(prefix + key, value);
}
if (initComponent) initComponent(model, scoped);
if (isServer) return;
var eventPrefix = (macroCtx['name'] || (ns + ':' + name)) + ':'
, cancelled;
function cancel() {
cancelled = true;
}
scoped.trigger = function(name) {
var args = [eventPrefix + name, scoped]
.concat(arraySlice.call(arguments, 1), cancel);
cancelled = false;
model.emit.apply(model, args);
return cancelled;
};
setTimeout(function() {
var elements = ctx.$elements
, dom = view.dom.componentDom()
, key, id;
for (key in elements) {
id = elements[key];
if (typeof id !== 'string') continue;
elements[key] = document.getElementById(id);
}
script.create(model, scoped, dom, elements);
}, 0);
}
function extendCtx(ctx, value, name, alias, index, isArray) {
var path = ctxPath(ctx, name, null, true)
, aliases;
ctx = extend(ctx, value);
ctx["this"] = value;
if (alias) {
aliases = ctx.$aliases = Object.create(ctx.$aliases);
aliases[alias] = ctx.$depth;
}
if (path) ctx.$paths = [path].concat(ctx.$paths);
if (name) ctx.$depth++;
if (index != null) {
ctx.$indices = [index].concat(ctx.$indices);
isArray = true;
}
if (isArray && ctx.$paths[0]) {
ctx.$paths[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, boundMacro) {
function conditionalRender(ctx, model, triggerPath, value, index, condition) {
if (condition) {
var renderCtx = extendCtx(ctx, value, name, alias, index);
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 parentMacroCtx = ctx.$macroCtx
, renderCtx, scope;
if (alias) {
scope = '_$component.' + view._uniqueId();
renderCtx = extendCtx(ctx, null, scope, alias);
createComponent(view, model, name[0], name[1], scope, renderCtx, macroCtx, boundMacro);
} else {
renderCtx = Object.create(ctx);
}
renderCtx.$macroCtx = parentMacroCtx ? extend(parentMacroCtx, macroCtx) : macroCtx;
return render(renderCtx, model, triggerPath);
}
}
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(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);
out += render(renderCtx, model, triggerPath);
}
return out;
}
}
throw new Error('Unknown block type: ' + type);
}
var objectToString = Object.prototype.toString;
function textFn(view, name, escape, macro) {
return function(ctx, model) {
var value = dataValue(view, ctx, model, name, macro)
, text = typeof value === 'string' ? value
: value == null ? ''
: value.toString === objectToString ? JSON.stringify(value)
: value.toString();
return escape ? escape(text) : text;
}
}
function sectionFn(view, queue) {
var render = renderer(view, reduceStack(queue.stack), queue.events)
, block = queue.block;
return partialFn(view, block.name, block.type, block.alias, render, null, block.macro);
}
function blockFn(view, sections) {
var len = sections.length;
if (!len) return;
if (len === 1) {
return sectionFn(view, sections[0]);
} else {
var fns = []
, i;
for (i = 0; i < len; i++) {
fns.push(sectionFn(view, sections[i]));
}
return 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 '';
}
}
}
function parseMarkup(type, attr, tagName, events, attrs, name) {
var parser = markup[type][attr]
, anyOut, anyParser, elOut, elParser, out;
if (!parser) return;
if (anyParser = parser['*']) {
anyOut = anyParser(events, attrs, name);
}
if (elParser = parser[tagName]) {
elOut = elParser(events, attrs, name);
}
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 boundMacroName(boundMacro, name) {
var macroVar = name.split('.')[0];
return boundMacro[macroVar];
}
function boundName(boundMacro, match, name) {
if (!(name && match.macro)) return match.bound && name;
if (~name.indexOf('(')) {
var args = pathFnArgs(name)
, i, len;
for (i = 0, len = args.length; i < len; i++) {
if (boundMacroName(boundMacro, args[i])) return name;
}
return false;
}
return boundMacroName(boundMacro, name);
}
function isPartial(view, partial) {
var arr = partial.split(':')
, partialNs = arr[0];
return arr.length >= 2 &&
(partialNs === view._selfNs || !!view._libraries[partialNs]);
}
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[partialNs].view;
partialView._uniqueId = function() {
return view._uniqueId();
};
partialView.dom = view.dom;
} else {
partialView = view;
}
return [partialNs, partialName, partialView];
}
function isNonvoid(view, partial, ns) {
var arr = splitPartial(view, partial, ns)
, partialName = arr[1]
, view = arr[2];
return !!view._findItem(partialName, ns, '_nonvoidComponents');
}
function pushVar(view, ns, stack, events, boundMacro, remainder, match, fn) {
var name = match.name
, partial = match.partial
, macro = match.macro
, escapeFn = match.escaped && escapeHtml
, attr, attrs, boundOut, last, tagName, wrap, render, parseName;
if (partial) {
var arr = splitPartial(view, partial, ns)
, partialNs = arr[0]
, partialName = arr[1]
, alias = partialNs === view._selfNs ? '' : 'self'
render = arr[2]._find(partialName, ns, boundMacro);
fn = partialFn(view, arr, 'partial', alias, render, match.macroCtx, null, boundMacro);
}
if (parseName = boundName(boundMacro, match, name)) {
last = stack[stack.length - 1];
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, parseName);
}
boundOut = parseMarkup('boundParent', '*', tagName, events, attrs, parseName);
if (boundOut) {
bindEventsById(events, macro, name, null, attrs, boundOut.method, boundOut.property);
}
}
addId(view, attrs);
if (!boundOut) {
bindEventsById(events, macro, name, fn, attrs, 'html', !fn && escapeFn, true);
}
}
pushVarFn(view, stack, fn, name, escapeFn, macro);
if (wrap) {
stack.push([
'marker'
, '$'
, { id: function() { return attrs._id } }
]);
}
}
function pushVarString(view, ns, stack, events, boundMacro, remainder, match, fn) {
var name = match.name
, escapeFn = !match.escaped && unescapeEntities;
function bindOnce(ctx) {
ctx.$onBind(events, name);
bindOnce = empty;
}
if (boundName(boundMacro, match, name)) {
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 boundMacro, lastQueue, queue;
if (end) {
lastQueue = queues.pop();
queue = queues.last();
queue.sections.push(lastQueue);
} else {
queue = queues.last();
}
if (start) {
boundMacro = Object.create(queue.boundMacro);
queues.push(queue = {
stack: []
, events: []
, block: block
, sections: []
, boundMacro: boundMacro
});
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 = queues.last().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, boundMacro, tagName, attrs, attr, value) {
if (typeof value === 'function') return;
var attrOut = parseMarkup('attr', attr, tagName, events, attrs, value) || {}
, parseName, 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);
}, boundMacro);
attrs[attr] = attr === 'id' ? function(ctx, model) {
return attrs._id = escapeAttribute(render(ctx, model));
} : function(ctx, model) {
return escapeAttribute(render(ctx, model));
}
return;
}
if (parseName = boundName(boundMacro, match, name)) {
boundOut = parseMarkup('bound', attr, tagName, events, attrs, parseName) || {};
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, value) {
var bound = false
, match = extractPlaceholder(value)
, name;
if (attr === 'content') {
throw new Error('components may not have an attribute named "content"');
}
if (match) {
if (match.pre || match.post) {
throw new Error('unimplemented: blocks in component attributes');
}
name = match.name;
bound = match.bound;
attrs[attr] = {$macroVar: name};
} 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;
}
return bound;
}
function parse(view, viewName, template, isString, onBind, boundMacro) {
if (boundMacro == null) boundMacro = {};
var queues, stack, events, onRender, push;
queues = [{
stack: stack = []
, events: events = []
, sections: []
, boundMacro: boundMacro
}];
queues.last = function() {
return queues[queues.length - 1];
};
function onStart(queue) {
stack = queue.stack;
events = queue.events;
boundMacro = queue.boundMacro;
}
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, bound, out, parser, value
if ('x-no-minify' in attrs) {
delete attrs['x-no-minify'];
minifyContent = false;
} else {
minifyContent = true;
}
if (isPartial(view, tagName)) {
for (attr in attrs) {
value = attrs[attr];
bound = parsePartialAttr(view, viewName, events, attrs, attr, value);
if (bound) {
boundMacro[attr] = attrs[attr].$macroVar;
}
}
block = {
partial: tagName
, macroCtx: attrs
};
if (isNonvoid(view, tagName, ns)) {
onBlock(true, false, block, queues, {onStart: onStart});
} else {
push(view, ns, stack, events, boundMacro, '', block);
}
return;
}
if (parser = markup.element[tagName]) {
out = parser(events, attrs);
if (out != null ? out.addId : void 0) {
addId(view, attrs);
}
}
for (attr in attrs) {
value = attrs[attr];
parseAttr(view, viewName, events, boundMacro, tagName, attrs, attr, value);
}
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, boundMacro, remainder, sections[0].block, fn);
}
, onContent: function(match) {
push(view, ns, stack, events, boundMacro, remainder, match);
}
});
if (post) return parseText(post);
}
function parseEnd(tag, tagName) {
if (isPartial(view, tagName)) {
onBlock(false, true, null, queues, {
onStart: onStart
, onEnd: function(queues) {
var queue = queues[0]
, block = queue.block;
block.macroCtx.content = renderer(view, reduceStack(queue.stack), queue.events);
push(view, ns, stack, events, boundMacro, '', 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);
}