UNPKG

13.3 kBJavaScriptView Raw
1/**
2 * @license
3 * MOST Web Framework 2.0 Codename Blueshift
4 * Copyright (c) 2017, THEMOST LP All rights reserved
5 *
6 * Use of this source code is governed by an BSD-3-Clause license that can be
7 * found in the LICENSE file at https://themost.io/license
8 */
9var _ = require('lodash');
10var HttpViewHelper = require('../helpers').HtmlViewHelper;
11var HttpNotFoundError = require('@themost/common/errors').HttpNotFoundError;
12var ejs = require('ejs');
13var path = require('path');
14var fs = require('fs');
15var Symbol = require('symbol');
16var DirectiveEngine = require('./../handlers/directive').DirectiveEngine;
17var PostExecuteResultArgs = require('./../handlers/directive').PostExecuteResultArgs;
18var HttpViewContext = require('./../mvc').HttpViewContext;
19var layoutFileProperty = Symbol();
20var viewProperty = Symbol();
21var scriptsProperty = Symbol();
22var stylesheetsProperty = Symbol();
23
24/**
25 * @this EjsEngine
26 * @param {string} result
27 * @param {*} data
28 * @param {Function} callback
29 */
30function postRender(result, data, callback) {
31 var directiveHandler = new DirectiveEngine();
32 var viewContext = new HttpViewContext(this.context);
33 viewContext.body = result;
34 viewContext.data = data;
35 var args = _.assign(new PostExecuteResultArgs(), {
36 "context": this.context,
37 "target": viewContext
38 });
39 directiveHandler.postExecuteResult(args, function(err) {
40 if (err) {
41 return callback(err);
42 }
43 return callback(null, viewContext.body);
44 });
45}
46/**
47 * @class
48 */
49function EjsLocals() {
50 this[scriptsProperty] = new Array();
51 this[stylesheetsProperty] = new Array();
52}
53/**
54 * @param {string} view
55 */
56EjsLocals.prototype.layout = function(view) {
57 // validate view
58 if (typeof view !== 'string') {
59 throw new TypeError('Include view must be a string');
60 }
61 // if view does not have extension e.g. ../shared/master
62 if (!/\.html\.ejs$/ig.test(view)) {
63 // add .html.ejs extension
64 this[layoutFileProperty] = view + '.html.ejs';
65 }
66 else {
67 this[layoutFileProperty] = view;
68 }
69};
70/**
71 * @param {string} view
72 * @param {*} data
73 */
74EjsLocals.prototype.partial = function(view, data){
75 if (typeof view !== 'string') {
76 throw new TypeError('Include view must be a string');
77 }
78 // if view does not have extension e.g. ../shared/master
79 if (!/\.html\.ejs$/ig.test(view)) {
80 // add .html.ejs extension
81 view = view + '.html.ejs';
82 }
83 if (typeof this[viewProperty] !== 'string') {
84 throw new TypeError('Current view must be a string');
85 }
86 // get include view file path
87 var includeFile = path.resolve(path.dirname(this[viewProperty]), view);
88 // get source
89 var source;
90 // if process running in development mode
91 if (process.env.NODE_ENV === 'development') {
92 // get original source
93 source = fs.readFileSync(includeFile, 'utf-8');
94 }
95 else {
96 // otherwise search cache
97 source = ejs.cache.get(includeFile);
98 // if source is already loaded do nothing
99 if (typeof source === 'undefined') {
100 // otherwise read file
101 source = fs.readFileSync(includeFile, 'utf-8');
102 // and set file to cache
103 ejs.cache.set(includeFile, source);
104 }
105 }
106 // if data is undefined
107 if (typeof data === 'undefined') {
108 // do nothing
109 return;
110 }
111 else {
112 // get context
113 var context;
114 if (this.html && this.html.context) {
115 context = this.html.context;
116 }
117 // if data is array
118 if (_.isArray(data)) {
119 return _.map(data, function(item) {
120 // init locals
121 var locals = _.assign(new EjsLocals(), {
122 // set current model
123 model: item,
124 // set view helper
125 html: new HttpViewHelper(context)
126 });
127 // render view
128 return ejs.render(source, locals);
129 }).join('\n');
130 }
131 else {
132 // init a new instance of EjsLocals class
133 var locals = _.assign(new EjsLocals(), {
134 // set current model
135 model: data,
136 // set view helper
137 html: new HttpViewHelper(context)
138 });
139 // render view
140 return ejs.render(source, locals);
141 }
142
143 }
144};
145
146/**
147 * @param {string} view
148 * @param {*=} data
149 */
150EjsLocals.prototype.include = function(view, data){
151 if (typeof view !== 'string') {
152 throw new TypeError('Include view must be a string');
153 }
154 // if view does not have extension e.g. ../shared/master
155 if (!/\.html\.ejs$/ig.test(view)) {
156 // add .html.ejs extension
157 view = view + '.html.ejs';
158 }
159 if (typeof this[viewProperty] !== 'string') {
160 throw new TypeError('Current view must be a string');
161 }
162 // get include view file path
163 var includeFile = path.resolve(path.dirname(this[viewProperty]), view);
164 // get source
165 var source;
166 // if process running in development mode
167 if (process.env.NODE_ENV === 'development') {
168 // get original source
169 source = fs.readFileSync(includeFile, 'utf-8');
170 }
171 else {
172 // otherwise search cache
173 source = ejs.cache.get(includeFile);
174 // if source is already loaded do nothing
175 if (typeof source === 'undefined') {
176 // otherwise read file
177 source = fs.readFileSync(includeFile, 'utf-8');
178 // and set file to cache
179 ejs.cache.set(includeFile, source);
180 }
181 }
182 // if data is undefined
183 if (typeof data === 'undefined') {
184 // render view with current locals
185 return ejs.render(source, this);
186 }
187 else {
188 // get context
189 var context;
190 if (this.html && this.html.context) {
191 context = this.html.context;
192 }
193 // init a new instance of EjsLocals class
194 var locals = _.assign(new EjsLocals(), {
195 // set current model
196 model: data,
197 // set view helper
198 html: new HttpViewHelper(context)
199 });
200 // render view
201 return ejs.render(source, locals);
202 }
203};
204
205EjsLocals.prototype.script = function(path, type) {
206 if (path) {
207 this[scriptsProperty].push('<script src="'+path+'"'+(type ? 'type="'+type+'"' : '')+'></script>');
208 }
209 return this;
210};
211
212EjsLocals.prototype.stylesheet = function(path, media) {
213 if (path) {
214 this[stylesheetsProperty].push('<link rel="stylesheet" href="'+path+'"'+(media ? 'media="'+media+'"' : '')+' />');
215 }
216 return this;
217};
218
219/**
220 * @class
221 * @param {HttpContext=} context
222 * @constructor
223 */
224function EjsEngine(context) {
225
226 /**
227 * @property
228 * @name EjsEngine#context
229 * @type HttpContext
230 * @description Gets or sets an instance of HttpContext that represents the current HTTP context.
231 */
232 /**
233 * @type {HttpContext}
234 */
235 var ctx = context;
236 Object.defineProperty(this,'context', {
237 get: function() {
238 return ctx;
239 },
240 set: function(value) {
241 ctx = value;
242 },
243 configurable:false,
244 enumerable:false
245 });
246}
247
248/**
249 * @returns {HttpContext}
250 */
251EjsEngine.prototype.getContext = function() {
252 return this.context;
253};
254
255/**
256 * Adds a EJS filter to filters collection.
257 * @param {string} name
258 * @param {Function} filterFunc
259 */
260EjsEngine.prototype.filter = function(name, filterFunc) {
261 ejs.filters[name] = filterFunc;
262};
263
264/**
265 *
266 * @param {string} filename
267 * @param {*=} data
268 * @param {Function} callback
269 */
270EjsEngine.prototype.render = function(filename, data, callback) {
271 var self = this;
272 var locals;
273 var source;
274 try {
275 if (process.env.NODE_ENV === 'development') {
276 source = fs.readFileSync(filename,'utf-8');
277 }
278 else {
279 source = ejs.cache.get(filename);
280 if (typeof source === 'undefined') {
281 //read file
282 source = fs.readFileSync(filename,'utf-8');
283 // set file to cache
284 ejs.cache.set(filename, source);
285 }
286 }
287
288 // init locals as an instance of EjsLocals
289 locals = _.assign(new EjsLocals(), {
290 model: data,
291 html:new HttpViewHelper(self.context)
292 });
293 // set current view propertry
294 locals[viewProperty] = filename;
295 //get view header (if any)
296 var matcher = /^(\s*)<%#(.*?)%>/;
297 var properties = {
298 /**
299 * @type {string|*}
300 */
301 layout:null
302 };
303 if (matcher.test(source)) {
304 var matches = matcher.exec(source);
305 properties = JSON.parse(matches[2]);
306 //remove match
307 source = source.replace(matcher,'');
308 // deprecated message
309 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\')%>.');
310 }
311 if (properties.layout) {
312 var layout;
313 if (/^\//.test(properties.layout)) {
314 //relative to application folder e.g. /views/shared/master.html.ejs
315 layout = self.context.getApplication().mapExecutionPath(properties.layout);
316 }
317 else {
318 //relative to view file path e.g. ./../master.html.html.ejs
319 layout = path.resolve(path.dirname(filename), properties.layout);
320 }
321 //set current view buffer (after rendering)
322 var body = ejs.render(source, locals);
323 // assign body
324 _.assign(locals, {
325 body: body
326 });
327 //render master layout
328 return ejs.renderFile(layout, locals, {
329 cache: process.env.NODE_ENV !== 'development'
330 }, function(err, result) {
331 try {
332 if (err) {
333 if (err.code === 'ENOENT') {
334 return callback(new HttpNotFoundError('Master view layout cannot be found'));
335 }
336 return callback(err);
337 }
338 return postRender.bind(self)(result, locals.model, function(err, finalResult) {
339 if (err) {
340 return callback(err);
341 }
342 return callback(null, finalResult);
343 });
344 }
345 catch (err) {
346 callback(err);
347 }
348 });
349 }
350 else {
351 // render
352 var htmlResult = ejs.render(source, locals);
353 // validate layout
354 if (typeof locals[layoutFileProperty] === 'string') {
355 // resolve layout file path (relative to this view)
356 var layoutFile = path.resolve(path.dirname(filename), locals[layoutFileProperty]);
357 // remove private layout attribute
358 delete locals[layoutFileProperty];
359 // assign body, scripts and stylesheets
360 _.assign(locals, {
361 body: htmlResult,
362 scripts: locals[scriptsProperty].join('\n'),
363 stylesheets: locals[stylesheetsProperty].join('\n'),
364 });
365 // render layout file
366 return ejs.renderFile(layoutFile, locals, {
367 cache: process.env.NODE_ENV !== 'development'
368 }, function(err, result) {
369 if (err) {
370 return callback(err);
371 }
372 // execute post render
373 return postRender.bind(self)(result, locals.model, function(err, finalResult) {
374 if (err) {
375 return callback(err);
376 }
377 return callback(null, finalResult);
378 });
379 });
380 }
381 return postRender.bind(self)(htmlResult, locals.model, function(err, finalResult) {
382 if (err) {
383 return callback(err);
384 }
385 return callback(null, finalResult);
386 });
387 }
388 }
389 catch (err) {
390 if (err.code === 'ENOENT') {
391 //throw not found exception
392 return callback(new HttpNotFoundError('View layout cannot be found.'));
393 }
394 return callback(err);
395 }
396};
397
398/**
399 * @static
400 * @param {HttpContext=} context
401 * @returns {EjsEngine}
402 */
403EjsEngine.createInstance = function(context) {
404 return new EjsEngine(context);
405};
406
407if (typeof exports !== 'undefined') {
408 module.exports.EjsEngine = EjsEngine;
409 /**
410 * @param {HttpContext=} context
411 * @returns {EjsEngine}
412 */
413 module.exports.createInstance = function(context) {
414 return EjsEngine.createInstance(context);
415 };
416}