var EventDispatcher = require('./EventDispatcher') , racer = require('racer') , Promise = racer.util.Promise , isProduction = racer.util.isProduction , merge = racer.util.merge , finishAfter = racer.util.async.finishAfter , Model = racer["protected"].Model , uglify = require('racer/node_modules/uglify-js') , files = require('./files') , htmlUtil = require('html-util') , escapeHtml = htmlUtil.escapeHtml , trimLeading = htmlUtil.trimLeading , refresh = require('./refresh.server') , errorHtml = refresh.errorHtml , cssError = refresh.cssError , templateError = refresh.templateError , View = module.exports = require('./View') , emptyModel = new Model , emptyRes = { getHeader: empty , setHeader: empty , write: empty , end: empty } , emptyPathMap = { id: empty } , emptyEventDispatcher = { bind: empty } emptyModel._commit = empty; emptyModel.bundle = empty; function empty() {} function escapeInlineScript(s) { return s.replace(/<\//g, '<\\/'); } function loadTemplatesScript(requirePath, templates, instances, libraryData) { return '(function() {\n' + 'var view = require("' + requirePath + '").view;\n' + 'view._makeAll(\n' + JSON.stringify(templates, null, 2) + ', ' + JSON.stringify(instances, null, 2) + ');\n' + 'view._makeComponents(\n' + JSON.stringify(libraryData, null, 2) + ');\n' + '})();'; } View.prototype.isServer = true; View.prototype.inline = function(fn) { return this._inline += uglify("(" + fn + ")()") + ';'; }; View.prototype._load = function(isStatic, callback) { var view = this , appFilename, clientName, count, errors, finish, js, options , promise, requirePath, root, libraries, fileInfo, loadTemplates; if (isProduction) { this._watch = false; this._load = function(isStatic, callback) { callback(); }; } else { this._watch = true; } // Use a promise to avoid simultaneously loading multiple times if (promise = this._loadPromise) { return promise.on(callback); } promise = this._loadPromise = (new Promise).on(callback); // Once loading is complete, make the files reload from disk the next time promise.on(function() { delete view._loadPromise; }); errors = {}; if (isStatic) { root = this._root; clientName = this._clientName; count = 2; finish = function() { if (--count) return; promise.resolve(); }; } else { appFilename = this._appFilename; options = this._derbyOptions || {}; fileInfo = files.parseName(appFilename, options); this._root = root = fileInfo.root; this._requirePath = requirePath = fileInfo.require; this._clientName = clientName = fileInfo.clientName; if (!clientName) promise.resolve(); count = 3; finish = function() { if (--count) return; // Templates are appended to the js bundle here so that it does // not have to be regenerated if only the template files are modified if (isProduction) loadTemplates = uglify(loadTemplates); js += ';' + loadTemplates; view._errors = errorHtml(errors) || ''; files.writeJs(root, js, options, function(err, jsFile, appHash) { if (err) throw err; view._jsFile = jsFile; view._appHash = appHash; promise.resolve(); }); }; if (this._js) { js = this._js; finish(); } else { files.js(appFilename, function(err, value, inline) { if (err) throw err; js = value; if (!isProduction) view._js = value; if (inline) view.inline("function(){" + inline + "}"); finish(); }); } } this._loadCss(root, clientName, function(err, css) { if (err) { css = ''; errors['CSS'] = cssError(err); } else { css = css ? '' : ''; } view._css = css; finish(); }); libraries = this._libraries; this._loadTemplates(root, clientName, function(err, templates, instances, libraryData) { if (err) errors['Template'] = templateError(err); loadTemplates = loadTemplatesScript(requirePath, templates, instances, libraryData); view._makeAll(templates, instances); view._makeComponents(libraryData); finish(); }); }; View.prototype._loadCss = function(root, clientName, callback) { files.css(root, clientName, isProduction, function(err, value) { value = isProduction ? trimLeading(value) : '\n' + value; callback(err, value); }); }; View.prototype._loadTemplates = function(root, clientName, callback) { var count = 1 , libraries = this._libraries , libraryData = {} , templates, instances, finish, libraryName, library for (libraryName in libraries) count++; finish = finishAfter(count, function(err) { callback(err, templates, instances, libraryData); }); files.templates(root, clientName, function(err, _templates, _instances) { if (err) { templates = {}; instances = {}; } else { templates = _templates; instances = _instances; } finish(err); }); for (libraryName in libraries) { library = libraries[libraryName]; files.library(library.root, function(err, components) { if (err) return finish(err); var libraryTemplates = {} , libraryInstances = {} , componentName, component; for (componentName in components) { component = components[componentName]; // TODO: Namespace component partials of each component merge(libraryTemplates, component.templates); merge(libraryInstances, component.instances); } libraryData[libraryName] = { templates: libraryTemplates , instances: libraryInstances }; finish(); }); } }; View.prototype.render = function(res) { var view = this , i, arg, ctx, isStatic, model, ns; if (res == null) res = emptyRes; for (i = 1; i <= 5; i++) { arg = arguments[i]; if (arg instanceof Model) { model = arg; } else if (typeof arg === 'object') { ctx = arg; } else if (typeof arg === 'string') { ns = arg; } else if (typeof arg === 'number') { res.statusCode = arg; } else if (typeof arg === 'boolean') { isStatic = arg; } } if (model == null) model = emptyModel; // Load templates, css, and scripts from files this._load(isStatic, function() { view._render(res, model, ns, ctx, isStatic); }); }; View.prototype._init = function(model) { // Initialize view & model for rendering model.__events = emptyEventDispatcher; model.__blockPaths = {}; model.__pathMap = emptyPathMap; this.model = model; this._idCount = 0; var libraries = this._libraries , name for (name in libraries) { libraries[name].view._init(model); } }; View.prototype._render = function(res, model, ns, ctx, isStatic) { this._init(model); if (!res.getHeader('content-type')) { res.setHeader('Content-Type', 'text/html; charset=utf-8'); } try { // The view.get function renders and sets event listeners var doctype = this.get('doctype', ns, ctx) , root = this.get('root', ns, ctx) , charset = this.get('charset', ns, ctx) , title = escapeHtml(this.get('title$s', ns, ctx)) , head = this.get('head', ns, ctx) , header = this.get('header', ns, ctx) , view = this , body, scripts, tail; // The first chunk includes everything through header. Head should contain // any meta tags and script tags, since it is included before CSS. // If there is a small amount of header HTML that will display well by itself, // it is a good idea to add this to the Header view so that it renders ASAP. res.write(doctype + root + charset + "" + title + "" + head + this._css + header); // Remaining HTML body = this.get('body', ns, ctx) + this.get('footer', ns, ctx); if (body.slice(0, 4) === '