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