1 |
|
2 |
|
3 |
|
4 | var path = require('path'),
|
5 | fs = require('fs'),
|
6 | exists = fs.exists || path.exists,
|
7 | crypto = require('crypto'),
|
8 | utils = require('./utils'),
|
9 | FormForResource = require('./form-for-resource.js'),
|
10 | _url = require('url');
|
11 |
|
12 |
|
13 |
|
14 |
|
15 | var htmlTagParams = utils.html_tag_params,
|
16 | safe_merge = utils.safe_merge,
|
17 | humanize = utils.humanize,
|
18 | undef;
|
19 |
|
20 |
|
21 |
|
22 |
|
23 | var regexps = {
|
24 | 'cached': /^cache\//,
|
25 | 'isHttp': /^https?:\/\/|\/\//
|
26 | },
|
27 | exts = {
|
28 | 'css': '.css',
|
29 | 'js' : '.js'
|
30 | },
|
31 | paths = {
|
32 | 'css': '/stylesheets/',
|
33 | 'js' : '/javascripts/'
|
34 | },
|
35 | merged = {
|
36 | stylesheets: {ext: exts.css},
|
37 | javascripts: {ext: exts.js}
|
38 | },
|
39 | globalContents = {};
|
40 |
|
41 |
|
42 |
|
43 |
|
44 | module.exports = new HelperSet(null);
|
45 | module.exports.HelperSet = HelperSet;
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 |
|
53 | function HelperSet(ctl) {
|
54 | var helpers = this;
|
55 | this.controller = ctl;
|
56 | this._contents = ctl ? new ContentsBuffer() : globalContents;
|
57 | this.htmlEscape = true;
|
58 | if (ctl) {
|
59 | this.controllerName = ctl.controllerName;
|
60 | this.actionName = ctl.actionName;
|
61 | this.pathTo = this.path_to = ctl.pathTo;
|
62 | this.t = ctl.t.bind(ctl);
|
63 | this.t.locale = ctl._t && ctl._t.locale;
|
64 | this.htmlEscape = ctl && ctl.compound.app && ctl.compound.app.enabled('escape html');
|
65 | }
|
66 |
|
67 | |
68 |
|
69 |
|
70 |
|
71 |
|
72 | this.csrfMetaTag = this.csrf_meta_tag = function() {
|
73 | return ctl && ctl.protectedFromForgery() ? [
|
74 | helpers.metaTag('csrf-param', ctl.req.csrfParam),
|
75 | helpers.metaTag('csrf-token', ctl.req.csrfToken)
|
76 | ].join('\n') : '';
|
77 | };
|
78 |
|
79 |
|
80 | Object.keys(HelperSet.prototype).forEach(function(name) {
|
81 | helpers[name] = HelperSet.prototype[name].bind(helpers);
|
82 | });
|
83 | }
|
84 |
|
85 | function ContentsBuffer() {
|
86 | var buf = this;
|
87 | Object.keys(globalContents).forEach(function(key) {
|
88 | buf[key] = [];
|
89 | globalContents[key].forEach(function (val) {
|
90 | buf[key].push(val);
|
91 | });
|
92 | });
|
93 | }
|
94 |
|
95 |
|
96 |
|
97 |
|
98 |
|
99 |
|
100 |
|
101 |
|
102 | module.exports.personalize = function(controller) {
|
103 | return new module.exports.HelperSet(controller);
|
104 | };
|
105 |
|
106 | HelperSet.prototype.metaTag = function (name, content, params) {
|
107 | var undef;
|
108 | params = params || {};
|
109 | if (content && typeof content === 'object') {
|
110 | params = content;
|
111 | content = undef || params.content;
|
112 | }
|
113 | if (name && typeof name === 'object') {
|
114 | params = name;
|
115 | name = undef || params.name;
|
116 | content = undef || params.content;
|
117 | }
|
118 | return genericTagSelfclosing('meta', {name: name, content: content}, params);
|
119 | };
|
120 |
|
121 |
|
122 |
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 |
|
129 |
|
130 |
|
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 | HelperSet.prototype.stylesheetLinkTag = function stylesheetLinkTag() {
|
137 | var args;
|
138 | var app = this.controller ? this.controller.app : null;
|
139 | if (!paths.css || !paths.stylesheets) {
|
140 | paths.css = app && this.controller.app.settings.cssDirectory || '/stylesheets/';
|
141 | paths.stylesheets = paths.css;
|
142 | }
|
143 |
|
144 | if (arguments[0] instanceof Array) {
|
145 | args = arguments[0];
|
146 | } else {
|
147 | args = Array.prototype.slice.call(arguments);
|
148 | }
|
149 | var options = {media: 'screen', rel: 'stylesheet', type: 'text/css'};
|
150 | var links = [];
|
151 | if (typeof args[args.length - 1] == 'object') {
|
152 | options = safe_merge(options, args.pop());
|
153 | }
|
154 | mergeFiles(app, 'stylesheets', args).forEach(function(file) {
|
155 | delete options.href;
|
156 |
|
157 | var href = checkFile(app, 'css', file);
|
158 | links.push(genericTagSelfclosing('link', options, { href: href }));
|
159 | });
|
160 | return links.join('\n ');
|
161 | };
|
162 | HelperSet.prototype.stylesheet_link_tag = HelperSet.prototype.stylesheetLinkTag;
|
163 |
|
164 |
|
165 |
|
166 |
|
167 |
|
168 |
|
169 |
|
170 |
|
171 |
|
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
177 |
|
178 |
|
179 | HelperSet.prototype.javascriptIncludeTag = function javascriptIncludeTag() {
|
180 | var helpers = this;
|
181 | var args;
|
182 | var app = this.controller ? this.controller.app : null;
|
183 | if (!paths.js || !paths.javascripts) {
|
184 | paths.js = app && this.controller.app.settings.jsDirectory || '/javascripts/';
|
185 | paths.javascripts = paths.js;
|
186 | }
|
187 | if (arguments[0] instanceof Array) {
|
188 | args = arguments[0];
|
189 | } else {
|
190 | args = Array.prototype.slice.call(arguments);
|
191 | }
|
192 | var options = {type: 'text/javascript'};
|
193 | if (typeof args[args.length - 1] == 'object') {
|
194 | options = safe_merge(options, args.pop());
|
195 | }
|
196 | var scripts = [];
|
197 | mergeFiles(app, 'javascripts', args).forEach(function(file) {
|
198 |
|
199 | var href = checkFile(app, 'js', file);
|
200 | delete options.src;
|
201 | scripts.push(helpers.tag('script', '', options, {src: href}));
|
202 | });
|
203 | return scripts.join('\n ');
|
204 | };
|
205 | HelperSet.prototype.javascript_include_tag = HelperSet.prototype.javascriptIncludeTag;
|
206 |
|
207 |
|
208 |
|
209 |
|
210 |
|
211 |
|
212 |
|
213 |
|
214 |
|
215 |
|
216 |
|
217 |
|
218 |
|
219 |
|
220 | function mergeFiles(app, scope, files) {
|
221 |
|
222 | if (!app || app.disabled('merge ' + scope)) {
|
223 | return files;
|
224 | }
|
225 | var ext = merged[scope].ext,
|
226 | result = [],
|
227 | shasum = crypto.createHash('sha1'),
|
228 | minify = [],
|
229 | directory = merged[scope].directory = paths[scope].replace(/^\/|\/$/g, '');
|
230 |
|
231 | files.forEach(function(file) {
|
232 | if (!regexps.isHttp.test(file)) {
|
233 | shasum.update(file);
|
234 | minify.push(file);
|
235 | } else {
|
236 | result.push(file);
|
237 | }
|
238 | });
|
239 |
|
240 |
|
241 | var digest = shasum.digest('hex');
|
242 |
|
243 | var cached = merged[scope][digest];
|
244 | var root = app.get(ext.substr(1) + ' app root') || app.root;
|
245 | if (cached || !fs.createWriteStream) {
|
246 |
|
247 | result.push('cache_' + digest);
|
248 | } else if (fs.existsSync(path.join(root, 'public', directory))) {
|
249 |
|
250 | if (!fs.existsSync(path.join(root, 'public', directory, 'cache_' + digest + ext))) {
|
251 |
|
252 | var stream = fs.createWriteStream(path.join(root, 'public', directory, 'cache_' + digest + ext));
|
253 | var counter = 0;
|
254 | var fileContents = {};
|
255 | minify.forEach(function(file) {
|
256 | var filename = path.join(root, 'public', directory, file + ext);
|
257 | exists(filename, function(exists) {
|
258 | if (exists) {
|
259 | counter += 1;
|
260 |
|
261 | fs.readFile(filename, 'utf8', function(err, data) {
|
262 | fileContents[file] = data;
|
263 | done();
|
264 | });
|
265 | }
|
266 | });
|
267 | });
|
268 | function done() {
|
269 | if (--counter === 0) {
|
270 | minify.forEach(function(file) {
|
271 | data = fileContents[file];
|
272 | stream.write('/* /' + directory + '/' + file + ext + ' */ \n');
|
273 | stream.write(data + '\n');
|
274 | });
|
275 |
|
276 | stream.end();
|
277 | }
|
278 | }
|
279 |
|
280 | stream.on('close', function() {
|
281 | merged[scope][digest] = ['cache', digest].join('_');
|
282 | });
|
283 |
|
284 | result.push(['cache', digest].join('_'));
|
285 | } else {
|
286 | merged[scope][digest] = ['cache', digest].join('_');
|
287 | }
|
288 | }
|
289 | return result;
|
290 | }
|
291 |
|
292 |
|
293 |
|
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 |
|
300 |
|
301 |
|
302 |
|
303 |
|
304 |
|
305 |
|
306 |
|
307 |
|
308 | HelperSet.prototype.linkTo = function linkTo(text, url, params) {
|
309 | ['remote', 'method', 'jsonp', 'confirm'].forEach(dataParam.bind(params));
|
310 | return this.tag('a', text, {href: url}, params);
|
311 | };
|
312 | HelperSet.prototype.link_to = HelperSet.prototype.linkTo;
|
313 |
|
314 | HelperSet.prototype.linkToRemote = function linkToRemote(text, url, params) {
|
315 | params = params || {};
|
316 | params.remote = true;
|
317 | return this.linkTo(text, url, params);
|
318 | };
|
319 |
|
320 |
|
321 |
|
322 |
|
323 |
|
324 |
|
325 |
|
326 |
|
327 |
|
328 |
|
329 | HelperSet.prototype.linkToIfNotCurrent = function linkTo(text, url, params) {
|
330 | if (url && url[0]=='/') url = url.substring(1);
|
331 | return (url.toLowerCase() == _url.parse( this.controller.request.url ).pathname.substring(1).toLowerCase() ) ? text : HelperSet.prototype.link_to(text, url, params) ;
|
332 | };
|
333 | HelperSet.prototype.link_to_if_not_current = HelperSet.prototype.linkToIfNotCurrent;
|
334 |
|
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 |
|
341 |
|
342 | HelperSet.prototype.formTag = function(params, block) {
|
343 | throw new Error('Helpers formTag and formFor(with block) are deprecated, use',
|
344 | 'block-less version of formFor helper, or formTagBegin and formTagEnd tags');
|
345 | };
|
346 | HelperSet.prototype.form_tag = HelperSet.prototype.formTag;
|
347 |
|
348 | HelperSet.prototype.formTagRemote = function(params) {
|
349 | params = params || {};
|
350 | params.remote = true;
|
351 | return this.formTag(params);
|
352 | };
|
353 |
|
354 |
|
355 |
|
356 |
|
357 |
|
358 |
|
359 |
|
360 |
|
361 |
|
362 | HelperSet.prototype.errorMessagesFor = function errorMessagesFor(resource, params) {
|
363 | var out = '';
|
364 | var h = this;
|
365 | params = params || {};
|
366 |
|
367 | if (resource.errors) {
|
368 | var cls = (params.class) ? params.class : 'alert alert-error';
|
369 | out += this.tag('div', this.html(printErrors()), {class: cls});
|
370 | }
|
371 |
|
372 | return out;
|
373 |
|
374 | function printErrors() {
|
375 | var msg = (params.message) ? params.message : 'Validation failed. Fix the following errors to continue:';
|
376 | var out = '<p>';
|
377 | out += h.tag('strong', msg);
|
378 | out += '</p>';
|
379 | for (var prop in resource.errors) {
|
380 | if (resource.errors.hasOwnProperty(prop)) {
|
381 | out += '<ul>';
|
382 | resource.errors[prop].forEach(function (msg) {
|
383 | out += h.tag('li', utils.camelize(prop, true) + ' ' + msg, {class: 'error-message'});
|
384 | });
|
385 | out += '</ul>';
|
386 | }
|
387 | }
|
388 | return out;
|
389 | }
|
390 | };
|
391 |
|
392 |
|
393 |
|
394 |
|
395 |
|
396 |
|
397 |
|
398 |
|
399 |
|
400 | HelperSet.prototype.fieldsFor = function (resource, formParams) {
|
401 | return new FormForResource(resource || {}, formParams, null, this);
|
402 | };
|
403 |
|
404 |
|
405 |
|
406 |
|
407 |
|
408 |
|
409 |
|
410 |
|
411 |
|
412 |
|
413 |
|
414 | HelperSet.prototype.formFor = function formFor(resource, params, block) {
|
415 | var self = this;
|
416 |
|
417 | if (resource && resource.constructor && resource.constructor.modelName) {
|
418 | if (typeof params !== 'object') {
|
419 | params = {};
|
420 | }
|
421 | if (!params.method) {
|
422 | params.method = resource && resource.id ? 'PUT' : 'POST';
|
423 | }
|
424 | if (!params.action) {
|
425 | params.action = this.controller.app.compound.map.pathTo[utils.underscore(resource.constructor.modelName)](resource);
|
426 | }
|
427 | }
|
428 |
|
429 | |
430 |
|
431 |
|
432 | if (block) {
|
433 | this.formTag(params, function () {
|
434 | if (block) self.fieldsFor(resource, params, block);
|
435 | });
|
436 | } else {
|
437 |
|
438 | return self.fieldsFor(resource, params);
|
439 | }
|
440 | };
|
441 | HelperSet.prototype.form_for = HelperSet.prototype.formFor;
|
442 |
|
443 | HelperSet.prototype.formForRemote = function(resource, params) {
|
444 | params = params || {};
|
445 | params.remote = true;
|
446 | return this.formFor(resource, params);
|
447 | };
|
448 |
|
449 |
|
450 |
|
451 |
|
452 |
|
453 |
|
454 |
|
455 |
|
456 | HelperSet.prototype.formTagBegin = function (params) {
|
457 |
|
458 | if (!params.method) {
|
459 | params.method = 'POST';
|
460 | }
|
461 |
|
462 |
|
463 | var method = params.method.toUpperCase();
|
464 | var _method = method;
|
465 |
|
466 | if (method != 'GET' && method != 'POST') {
|
467 | _method = method;
|
468 | params.method = 'POST';
|
469 | }
|
470 |
|
471 |
|
472 | ['remote', 'jsonp', 'confirm'].forEach(dataParam.bind(params));
|
473 |
|
474 |
|
475 | var html = '<form' + htmlTagParams(params) + '>';
|
476 | html += this.csrfTag();
|
477 |
|
478 |
|
479 | if(_method !== params.method) {
|
480 | html += HelperSet.prototype.inputTag({type: "hidden", name: "_method", value: _method });
|
481 | }
|
482 | return html;
|
483 | };
|
484 |
|
485 | HelperSet.prototype.formTagRemoteBegin = function(params) {
|
486 | params = params || {};
|
487 | params.remote = true;
|
488 | return this.formTagBegin(params);
|
489 | };
|
490 |
|
491 |
|
492 |
|
493 |
|
494 |
|
495 |
|
496 |
|
497 | HelperSet.prototype.formTagEnd = function (params) {
|
498 | return '</form>';
|
499 | };
|
500 |
|
501 |
|
502 |
|
503 |
|
504 |
|
505 |
|
506 |
|
507 |
|
508 |
|
509 |
|
510 | HelperSet.prototype.inputTag = function (params, override) {
|
511 | return '<input' + htmlTagParams(params, override) + ' />';
|
512 | };
|
513 | HelperSet.prototype.input_tag = HelperSet.prototype.inputTag;
|
514 |
|
515 |
|
516 |
|
517 |
|
518 |
|
519 |
|
520 |
|
521 |
|
522 |
|
523 |
|
524 | HelperSet.prototype.textareaTag = function (value, params, override) {
|
525 | if (typeof value === 'object') {
|
526 | params = value;
|
527 | override = params;
|
528 | value = params.value;
|
529 | }
|
530 | return this.tag('textarea', value || '', params, override);
|
531 | };
|
532 |
|
533 |
|
534 |
|
535 |
|
536 |
|
537 |
|
538 |
|
539 |
|
540 |
|
541 |
|
542 |
|
543 |
|
544 |
|
545 |
|
546 | HelperSet.prototype.labelTag = function (text, params, override) {
|
547 | return this.tag('label', text, params, override);
|
548 | };
|
549 | HelperSet.prototype.label_tag = HelperSet.prototype.labelTag;
|
550 |
|
551 |
|
552 |
|
553 |
|
554 |
|
555 |
|
556 |
|
557 |
|
558 |
|
559 |
|
560 |
|
561 |
|
562 | HelperSet.prototype.submitTag = function (text, params) {
|
563 | return this.inputTag({value: text, type: 'submit'}, params);
|
564 | };
|
565 |
|
566 |
|
567 |
|
568 |
|
569 |
|
570 |
|
571 |
|
572 |
|
573 |
|
574 |
|
575 |
|
576 |
|
577 |
|
578 |
|
579 |
|
580 |
|
581 | HelperSet.prototype.buttonTag = function (text, params) {
|
582 | return this.tag('button', text, params);
|
583 | };
|
584 |
|
585 |
|
586 |
|
587 |
|
588 |
|
589 |
|
590 |
|
591 | HelperSet.prototype.csrfTag = function () {
|
592 | return '<input type="hidden" name="' + this.controller.req.csrfParam + '" value="' + this.controller.req.csrfToken + '" />';
|
593 | };
|
594 | HelperSet.prototype.csrf_tag = HelperSet.prototype.csrfTag;
|
595 |
|
596 |
|
597 |
|
598 |
|
599 |
|
600 |
|
601 |
|
602 |
|
603 |
|
604 |
|
605 |
|
606 |
|
607 |
|
608 |
|
609 |
|
610 | HelperSet.prototype.selectTag = function (innerOptions, params, override) {
|
611 | return this.tag('select', html(innerOptions), params, override);
|
612 | };
|
613 | HelperSet.prototype.select_tag = HelperSet.prototype.selectTag;
|
614 |
|
615 |
|
616 |
|
617 |
|
618 |
|
619 |
|
620 |
|
621 |
|
622 |
|
623 |
|
624 |
|
625 |
|
626 |
|
627 |
|
628 |
|
629 | HelperSet.prototype.optionTag = function (text, params, override) {
|
630 | return this.tag('option', text, params, override);
|
631 | };
|
632 | HelperSet.prototype.option_tag = HelperSet.prototype.optionTag;
|
633 |
|
634 |
|
635 |
|
636 |
|
637 |
|
638 |
|
639 |
|
640 |
|
641 |
|
642 |
|
643 |
|
644 |
|
645 | HelperSet.prototype.matcher = function (pattern, positive, negative) {
|
646 | negative = negative || '';
|
647 | return function (value) {
|
648 | return value === pattern ? positive : negative;
|
649 | };
|
650 | };
|
651 |
|
652 | HelperSet.prototype.icon = function (type, params) {
|
653 | return this.tag('i', '', {class: 'icon-' + type}, params) + ' ';
|
654 | };
|
655 |
|
656 | HelperSet.prototype.imageTag = function (src, params) {
|
657 | return genericTagSelfclosing('img', {src: src}, params);
|
658 | };
|
659 |
|
660 |
|
661 |
|
662 |
|
663 |
|
664 |
|
665 |
|
666 |
|
667 |
|
668 | HelperSet.prototype.anchor = function anchor(name, params) {
|
669 | params = params || {};
|
670 | params.name = name;
|
671 | return this.linkTo('', '', params);
|
672 | };
|
673 |
|
674 |
|
675 |
|
676 |
|
677 |
|
678 |
|
679 |
|
680 |
|
681 |
|
682 |
|
683 |
|
684 |
|
685 |
|
686 |
|
687 |
|
688 |
|
689 |
|
690 |
|
691 |
|
692 |
|
693 |
|
694 | HelperSet.prototype.contentFor = function contentFor(name, content) {
|
695 | if (content) {
|
696 | this._contents[name] = this._contents[name] || [];
|
697 | this._contents[name].push(content);
|
698 | } else {
|
699 | return (this._contents[name] || []).join('');
|
700 | }
|
701 | };
|
702 |
|
703 |
|
704 |
|
705 |
|
706 |
|
707 |
|
708 |
|
709 |
|
710 |
|
711 |
|
712 |
|
713 |
|
714 |
|
715 |
|
716 |
|
717 | function genericTag(name, inner, params, override) {
|
718 | return html('<' + name + htmlTagParams(params, override) + '>' + this.text(inner) + '</' + name + '>');
|
719 | }
|
720 | HelperSet.prototype.tag = genericTag;
|
721 |
|
722 | function html(res) {
|
723 | res = new String(res);
|
724 | res.toHtmlString = function() {
|
725 | return this;
|
726 | };
|
727 | return res;
|
728 | }
|
729 | HelperSet.prototype.html = html;
|
730 |
|
731 |
|
732 |
|
733 |
|
734 |
|
735 |
|
736 |
|
737 |
|
738 |
|
739 | function genericTagSelfclosing(name, params, override) {
|
740 | return html('<' + name + htmlTagParams(params, override) + ' />');
|
741 | }
|
742 |
|
743 |
|
744 |
|
745 |
|
746 |
|
747 |
|
748 |
|
749 | function dataParam(key) {
|
750 | if (this[key]) {
|
751 | this['data-' + key] = this[key];
|
752 | delete this[key];
|
753 | }
|
754 | }
|
755 |
|
756 |
|
757 |
|
758 |
|
759 |
|
760 |
|
761 |
|
762 | function sanitizeHTML(text) {
|
763 | if (!this.htmlEscape) return text;
|
764 | if (typeof text === 'object') {
|
765 | if (text instanceof String && text.toHtmlString) {
|
766 | return text.toHtmlString();
|
767 | }
|
768 | text = JSON.stringify(text, null, ' ');
|
769 | }
|
770 | return text.replace(/&/g, '&').replace(/>/g, '>').replace(/</g, '<');
|
771 | }
|
772 | HelperSet.prototype.sanitize = sanitizeHTML;
|
773 | HelperSet.prototype.text = sanitizeHTML;
|
774 |
|
775 |
|
776 |
|
777 |
|
778 |
|
779 |
|
780 |
|
781 |
|
782 | function checkFile(app, type, file) {
|
783 | var isExternalFile = regexps.isHttp.test(file),
|
784 | isCached = file.match(regexps.cached),
|
785 | href = !isExternalFile ? paths[type] + file + exts[type] : file;
|
786 | var appprefix;
|
787 | if (!app) {
|
788 | appprefix = '';
|
789 | } else if (app.path) {
|
790 | appprefix = app.path();
|
791 | } else {
|
792 | appprefix = app.set('basepath') || '';
|
793 | }
|
794 | return isExternalFile ? href : appprefix + href;
|
795 | }
|
796 |
|