UNPKG

9.38 kBJavaScriptView Raw
1var task = require ('./base'),
2 path = require ('path'),
3 fs = require ('fs'),
4 util = require ('util'),
5 stream = require('stream');
6
7// TODO: write a message
8
9var presenters = {},
10 defaultTemplateDir = (typeof project !== "undefined" && project.config && project.config.templateDir) || 'share/presentation',
11 isWatched = (typeof project !== "undefined" && project.config && project.config.debug);
12
13/**
14 * @class task.presenterTask
15 * @extends task.task
16 *
17 * This is a type of task that sends a rendered template as an HTTP response.
18 *
19 * Implementation specific by definition.
20 */
21var presenterTask = module.exports = function (config) {
22
23 this.headers = {};
24 this.init (config);
25
26 if (config.headers) util.extend (this.headers, config.headers);
27};
28
29util.inherits (presenterTask, task);
30
31var cache = {};
32
33util.extend (presenterTask.prototype, {
34
35 defaultTemplateDir: defaultTemplateDir,
36 /**
37 * @private
38 */
39 readTemplate: function (templateIO, cb) {
40 templateIO.readFile (function (err, data) {
41 cb.call (this, err, data);
42 });
43 },
44
45 isInStaticDir: function (filePath) {
46 var httpStatic;
47 try {
48 httpStatic =
49 project.config.initiator.http.static.root.path ||
50 project.config.initiator.http.static.root;
51 } catch (e) {}
52
53 if (httpStatic) {
54 var rootPath = project.root.path;
55 httpStatic = path.resolve(rootPath, httpStatic);
56
57 var dirName = filePath;
58
59 while (dirName != rootPath) {
60 dirName = path.dirname(dirName);
61 if (dirName == httpStatic) {
62 return true;
63 break;
64 }
65 }
66 }
67 return false;
68 },
69
70 getAbsPath: function () {
71 return path.resolve(
72 project.root.path, this.defaultTemplateDir, this.file
73 );
74 },
75
76 getTemplateIO: function (callback) {
77 var self = this;
78 var defTemplate = this.getAbsPath();
79 var rootTemplate = path.resolve(project.root.path, this.file);
80
81 function isTemplateOk (templateIO) {
82 // warn if file is in static HTTP directory
83
84 if (self.isInStaticDir(templateIO)) {
85 throw new Error(
86 'Publicly accessible template file at '+templateIO+'!'
87 );
88 }
89 callback (project.root.fileIO (templateIO));
90 }
91
92 fs.exists(defTemplate, function (exists) {
93 if (exists) {
94 isTemplateOk (defTemplate);
95 } else {
96 fs.exists(rootTemplate, function (exists) {
97 if (exists) {
98 isTemplateOk (rootTemplate);
99 } else {
100 var statusCode = self.response.statusCode;
101 self.response.statusCode = (statusCode >= 200 && statusCode <= 300) ? 500 : statusCode;
102 // if we have good response, but no template file, this is failure.
103 // redirects, 4xx and 5xx codes seems ok without template
104 // self.response.writeHead ((statusCode >= 200 && statusCode <= 300) ? 500 : statusCode);
105 self.renderResult (null, "failure");
106 self.emit ("error", "template not found in '" + defTemplate + "' or '" + rootTemplate + "'");
107 // self.failed ();
108 }
109
110 });
111 }
112 });
113 },
114
115 renderFile: function () {
116 var self = this;
117
118 this.getTemplateIO(function (templateIO) {
119 templateIO.readFile(function (err, data) {
120 if (err) {
121 // self.response.writeHead (self.response.statusCode);
122 self.renderResult (null, "failure");
123 self.emit ("error", "template error: can't access file " + templateIO.path);
124 } else {
125 self.renderResult(data.toString());
126 }
127 });
128 });
129 },
130
131 /**
132 * @private
133 */
134 // TODO: add cache management
135 renderCompile: function() {
136 var self = this;
137
138 if (self.file in cache && cache[self.file]) {
139 self.renderProcess(cache[self.file]);
140 return;
141 }
142
143 var templateIO = this.getTemplateIO(function (templateIO) {
144 self.readTemplate (templateIO, function (err, tpl) {
145 if (err) {
146 console.error ("can't access file %s", templateIO.path);
147 // process.kill (); // bad idea
148 return;
149 }
150
151 var tplStr = tpl.toString();
152
153 // compile class method must return function. we call
154 // this function with presentation data. if your renderer
155 // doesn't have such function, you must extend renderer
156 // via renderer.prototype.compile
157 var compileMethod = self.compileMethod || 'compile';
158
159 if (!presenters[self.type])
160 presenters[self.type] = require (self.moduleName || self.type);
161
162 if (!presenters[self.type][compileMethod]) {
163 console.error (
164 'renderer \"' + self.type +
165 '\" doesn\'t have a template' +
166 'compilation method named \"' +
167 compileMethod + '\"'
168 );
169 }
170
171 if (isWatched) {
172
173 self.emit ('log', 'setting up watch for presentation file');
174 fs.watch (self.getAbsPath(), function () {
175 self.emit ('log', 'presentation file is changed');
176 delete cache[self.file];
177 });
178
179 }
180
181 cache[self.file] = presenters[self.type][compileMethod](
182 tplStr, self.compileParams || {}
183 );
184
185 if (self.renderMethod) {
186 self.renderProcess(cache[self.file][self.renderMethod].bind(cache[self.file]))
187 } else {
188 self.renderProcess(cache[self.file]);
189 }
190
191 });
192 });
193 },
194
195 /**
196 * @private
197 */
198 renderProcess: function(render) {
199
200 var responseData;
201 try {
202 responseData = render (this.vars);
203 } catch (e) {
204 this.emit ('error', e);
205 // console.log (e);
206 }
207 this.renderResult (
208 responseData
209 );
210
211 },
212
213 /**
214 * @private
215 */
216
217 renderResult: function(result, failure) {
218 if (this.headers) {
219 for (var key in this.headers) {
220 this.response.setHeader(key, this.headers[key]);
221 }
222 }
223 this.headers.connection = 'close';
224
225 if (this.verbose)
226 console.log (this.headers, result);
227
228 if (!result) {
229 this.response.end();
230 } else if (result instanceof stream) {
231 result.pipe (this.response);
232
233 result.on ('error', function (err) {
234 // console.log (err);
235 this.failed (err);
236 }.bind (this));
237
238 result.on ('close', function () {
239 this.completed();
240 }.bind (this));
241
242 return;
243 } else {
244 this.response.end(result);
245 }
246
247 if (!failure) {
248 this.completed();
249 } else {
250 this.failed();
251 }
252
253 },
254
255 /**
256 * @method run
257 * Renders the template from {@link #file} and sends the result
258 * as the content of the {@link #response}.
259 */
260 run: function () {
261
262 var self = this;
263
264 /**
265 * @cfg {String} file The template file name.
266 */
267
268 /**
269 * @cfg {String} type Template type. Tries to guess the type
270 * by the {@link #file} extension.
271 *
272 * Possible values:
273 *
274 * - `ejs`, EJS template
275 * - `json`, JSON string
276 * - `asis`, plain text.
277 * - `fileAsIs`, file from disk (please provide `file` param)
278 */
279
280 /**
281 * @cfg {http.ClientResponse} response (required) The response object.
282 *
283 * This task doesn't populate the {@link #produce}
284 * field of the dataflow. Instead, it sends the result via HTTP.
285 */
286
287 /**
288 * @cfg {String} contentType The MIME type of the response content.
289 *
290 * Default values depend on the template {@link #type}.
291 */
292
293 /**
294 * @cfg {Object} headers http headers to send
295 *
296 */
297
298 /**
299 * @cfg {String} code http status code to overrride current one
300 *
301 */
302
303 if (self.code) {
304 self.response.statusCode = self.code;
305 }
306
307 if (!self.type) {
308 if (self.file) {
309 // guess on file name
310 self.type = self.file.substring (self.file.lastIndexOf ('.') + 1);;
311 self.emit ('log', 'guessed ' + self.type + ' presenter type from filename: ' + self.file);
312 } else {
313 // if (self.response.statusCode > 200) {
314 self.renderResult ();
315 return;
316 // }
317 // TODO: throw error in case of 2xx response code
318 }
319 }
320
321 // TODO: lowercase all headers
322
323 switch (self.type.toLowerCase()) {
324 case 'html':
325 self.setContentType('text/html; charset=utf-8');
326 self.renderFile();
327 break;
328
329 case 'ejs':
330 if (!self.compileParams) { // compileParams can be defined in task initConfig
331 self.compileParams = {filename: path.resolve(
332 project.root.path, self.defaultTemplateDir, self.file
333 )};
334 }
335 case 'jade':
336 case 'mustache':
337 case 'handlebars':
338
339 self.setContentType(self.headers['content-type'] || 'text/html; charset=utf-8');
340
341 self.renderCompile();
342 break;
343
344 case 'hogan':
345 self.setContentType(self.headers['content-type'] || 'text/html; charset=utf-8');
346 self.moduleName = 'hogan.js';
347 self.renderMethod = 'render';
348
349 self.renderCompile();
350 break;
351
352 case 'json':
353 self.setContentType('application/json; charset=utf-8');
354 self.renderResult (
355 JSON.stringify (self.vars)
356 );
357 break;
358
359 case 'fileasis':
360 var mmm;
361 try {
362 mmm = require ('mmmagic');
363 } catch (e) {
364 console.error ("module 'mmmagic' not found.",
365 "this module required if you plan to use fileAsIs presenter type");
366 process.kill();
367 }
368
369 var Magic = mmm.Magic;
370
371 var magic = new Magic(mmm.MAGIC_MIME_TYPE);
372 magic.detectFile(self.file, function(err, contentType) {
373 if (err) throw err;
374
375 self.setContentType(contentType);
376 var fileStream = fs.createReadStream(self.file);
377 self.renderResult (fileStream);
378 });
379
380 break;
381
382 case 'asis':
383 default:
384 if (!self.headers['content-type']) {
385 var contentType = (self.contentType) ? self.contentType : 'text/plain';
386
387 if (!self.noUTF8 || contentType.indexOf ('application/') != 0) {
388 contentType += '; charset=utf-8';
389 }
390
391 self.setContentType(contentType);
392 }
393
394 self.renderResult (self.vars);
395 break;
396 }
397 },
398
399 setContentType: function(value) {
400 this.headers['content-type'] = value;
401 }
402});