mirror of
https://github.com/mgerb/mywebsite
synced 2026-01-12 10:52:47 +00:00
1430 lines
39 KiB
JavaScript
1430 lines
39 KiB
JavaScript
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 '<!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
|
|
, _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 <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;
|
|
|
|
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] += '<!--' + item[1];
|
|
i = pushValue(html, i, item[2].id, false, !item[1]);
|
|
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 = '<!--' + scope + '-->'
|
|
, 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('<derby:view> 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 <derby:view view={{...}}>
|
|
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);
|
|
}
|