UNPKG

@themost/web

Version:

MOST Web Framework 2.0 - Web Server Module

417 lines (403 loc) 13.3 kB
/** * @license * MOST Web Framework 2.0 Codename Blueshift * Copyright (c) 2017, THEMOST LP All rights reserved * * Use of this source code is governed by an BSD-3-Clause license that can be * found in the LICENSE file at https://themost.io/license */ var _ = require('lodash'); var HttpViewHelper = require('../helpers').HtmlViewHelper; var HttpNotFoundError = require('@themost/common/errors').HttpNotFoundError; var ejs = require('ejs'); var path = require('path'); var fs = require('fs'); var Symbol = require('symbol'); var DirectiveEngine = require('./../handlers/directive').DirectiveEngine; var PostExecuteResultArgs = require('./../handlers/directive').PostExecuteResultArgs; var HttpViewContext = require('./../mvc').HttpViewContext; var layoutFileProperty = Symbol(); var viewProperty = Symbol(); var scriptsProperty = Symbol(); var stylesheetsProperty = Symbol(); /** * @this EjsEngine * @param {string} result * @param {*} data * @param {Function} callback */ function postRender(result, data, callback) { var directiveHandler = new DirectiveEngine(); var viewContext = new HttpViewContext(this.context); viewContext.body = result; viewContext.data = data; var args = _.assign(new PostExecuteResultArgs(), { "context": this.context, "target": viewContext }); directiveHandler.postExecuteResult(args, function(err) { if (err) { return callback(err); } return callback(null, viewContext.body); }); } /** * @class */ function EjsLocals() { this[scriptsProperty] = new Array(); this[stylesheetsProperty] = new Array(); } /** * @param {string} view */ EjsLocals.prototype.layout = function(view) { // validate view if (typeof view !== 'string') { throw new TypeError('Include view must be a string'); } // if view does not have extension e.g. ../shared/master if (!/\.html\.ejs$/ig.test(view)) { // add .html.ejs extension this[layoutFileProperty] = view + '.html.ejs'; } else { this[layoutFileProperty] = view; } }; /** * @param {string} view * @param {*} data */ EjsLocals.prototype.partial = function(view, data){ if (typeof view !== 'string') { throw new TypeError('Include view must be a string'); } // if view does not have extension e.g. ../shared/master if (!/\.html\.ejs$/ig.test(view)) { // add .html.ejs extension view = view + '.html.ejs'; } if (typeof this[viewProperty] !== 'string') { throw new TypeError('Current view must be a string'); } // get include view file path var includeFile = path.resolve(path.dirname(this[viewProperty]), view); // get source var source; // if process running in development mode if (process.env.NODE_ENV === 'development') { // get original source source = fs.readFileSync(includeFile, 'utf-8'); } else { // otherwise search cache source = ejs.cache.get(includeFile); // if source is already loaded do nothing if (typeof source === 'undefined') { // otherwise read file source = fs.readFileSync(includeFile, 'utf-8'); // and set file to cache ejs.cache.set(includeFile, source); } } // if data is undefined if (typeof data === 'undefined') { // do nothing return; } else { // get context var context; if (this.html && this.html.context) { context = this.html.context; } // if data is array if (_.isArray(data)) { return _.map(data, function(item) { // init locals var locals = _.assign(new EjsLocals(), { // set current model model: item, // set view helper html: new HttpViewHelper(context) }); // render view return ejs.render(source, locals); }).join('\n'); } else { // init a new instance of EjsLocals class var locals = _.assign(new EjsLocals(), { // set current model model: data, // set view helper html: new HttpViewHelper(context) }); // render view return ejs.render(source, locals); } } }; /** * @param {string} view * @param {*=} data */ EjsLocals.prototype.include = function(view, data){ if (typeof view !== 'string') { throw new TypeError('Include view must be a string'); } // if view does not have extension e.g. ../shared/master if (!/\.html\.ejs$/ig.test(view)) { // add .html.ejs extension view = view + '.html.ejs'; } if (typeof this[viewProperty] !== 'string') { throw new TypeError('Current view must be a string'); } // get include view file path var includeFile = path.resolve(path.dirname(this[viewProperty]), view); // get source var source; // if process running in development mode if (process.env.NODE_ENV === 'development') { // get original source source = fs.readFileSync(includeFile, 'utf-8'); } else { // otherwise search cache source = ejs.cache.get(includeFile); // if source is already loaded do nothing if (typeof source === 'undefined') { // otherwise read file source = fs.readFileSync(includeFile, 'utf-8'); // and set file to cache ejs.cache.set(includeFile, source); } } // if data is undefined if (typeof data === 'undefined') { // render view with current locals return ejs.render(source, this); } else { // get context var context; if (this.html && this.html.context) { context = this.html.context; } // init a new instance of EjsLocals class var locals = _.assign(new EjsLocals(), { // set current model model: data, // set view helper html: new HttpViewHelper(context) }); // render view return ejs.render(source, locals); } }; EjsLocals.prototype.script = function(path, type) { if (path) { this[scriptsProperty].push('<script src="'+path+'"'+(type ? 'type="'+type+'"' : '')+'></script>'); } return this; }; EjsLocals.prototype.stylesheet = function(path, media) { if (path) { this[stylesheetsProperty].push('<link rel="stylesheet" href="'+path+'"'+(media ? 'media="'+media+'"' : '')+' />'); } return this; }; /** * @class * @param {HttpContext=} context * @constructor */ function EjsEngine(context) { /** * @property * @name EjsEngine#context * @type HttpContext * @description Gets or sets an instance of HttpContext that represents the current HTTP context. */ /** * @type {HttpContext} */ var ctx = context; Object.defineProperty(this,'context', { get: function() { return ctx; }, set: function(value) { ctx = value; }, configurable:false, enumerable:false }); } /** * @returns {HttpContext} */ EjsEngine.prototype.getContext = function() { return this.context; }; /** * Adds a EJS filter to filters collection. * @param {string} name * @param {Function} filterFunc */ EjsEngine.prototype.filter = function(name, filterFunc) { ejs.filters[name] = filterFunc; }; /** * * @param {string} filename * @param {*=} data * @param {Function} callback */ EjsEngine.prototype.render = function(filename, data, callback) { var self = this; var locals; var source; try { if (process.env.NODE_ENV === 'development') { source = fs.readFileSync(filename,'utf-8'); } else { source = ejs.cache.get(filename); if (typeof source === 'undefined') { //read file source = fs.readFileSync(filename,'utf-8'); // set file to cache ejs.cache.set(filename, source); } } // init locals as an instance of EjsLocals locals = _.assign(new EjsLocals(), { model: data, html:new HttpViewHelper(self.context) }); // set current view propertry locals[viewProperty] = filename; //get view header (if any) var matcher = /^(\s*)<%#(.*?)%>/; var properties = { /** * @type {string|*} */ layout:null }; if (matcher.test(source)) { var matches = matcher.exec(source); properties = JSON.parse(matches[2]); //remove match source = source.replace(matcher,''); // deprecated message console.log('INFO', 'Layout syntax e.g. <%# { "layout":"../shared/master.html.ejs" } %> is deprecated and it\'s going to be removed in a future version. Use layout() method instead e.g. <% layout(\'../shared/master\')%>.'); } if (properties.layout) { var layout; if (/^\//.test(properties.layout)) { //relative to application folder e.g. /views/shared/master.html.ejs layout = self.context.getApplication().mapExecutionPath(properties.layout); } else { //relative to view file path e.g. ./../master.html.html.ejs layout = path.resolve(path.dirname(filename), properties.layout); } //set current view buffer (after rendering) var body = ejs.render(source, locals); // assign body _.assign(locals, { body: body }); //render master layout return ejs.renderFile(layout, locals, { cache: process.env.NODE_ENV !== 'development' }, function(err, result) { try { if (err) { if (err.code === 'ENOENT') { return callback(new HttpNotFoundError('Master view layout cannot be found')); } return callback(err); } return postRender.bind(self)(result, locals.model, function(err, finalResult) { if (err) { return callback(err); } return callback(null, finalResult); }); } catch (err) { callback(err); } }); } else { // render var htmlResult = ejs.render(source, locals); // validate layout if (typeof locals[layoutFileProperty] === 'string') { // resolve layout file path (relative to this view) var layoutFile = path.resolve(path.dirname(filename), locals[layoutFileProperty]); // remove private layout attribute delete locals[layoutFileProperty]; // assign body, scripts and stylesheets _.assign(locals, { body: htmlResult, scripts: locals[scriptsProperty].join('\n'), stylesheets: locals[stylesheetsProperty].join('\n'), }); // render layout file return ejs.renderFile(layoutFile, locals, { cache: process.env.NODE_ENV !== 'development' }, function(err, result) { if (err) { return callback(err); } // execute post render return postRender.bind(self)(result, locals.model, function(err, finalResult) { if (err) { return callback(err); } return callback(null, finalResult); }); }); } return postRender.bind(self)(htmlResult, locals.model, function(err, finalResult) { if (err) { return callback(err); } return callback(null, finalResult); }); } } catch (err) { if (err.code === 'ENOENT') { //throw not found exception return callback(new HttpNotFoundError('View layout cannot be found.')); } return callback(err); } }; /** * @static * @param {HttpContext=} context * @returns {EjsEngine} */ EjsEngine.createInstance = function(context) { return new EjsEngine(context); }; if (typeof exports !== 'undefined') { module.exports.EjsEngine = EjsEngine; /** * @param {HttpContext=} context * @returns {EjsEngine} */ module.exports.createInstance = function(context) { return EjsEngine.createInstance(context); }; }