UNPKG

20 kBJavaScriptView Raw
1
2/*!
3 * Jade - Language Independent Templates.
4 * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
5 * MIT Licensed
6 */
7
8/**
9 * Library version.
10 */
11
12exports.version = '0.0.2';
13
14/**
15 * Module dependencies.
16 */
17
18var sys = require('sys'),
19 fs = require('fs');
20
21/**
22 * Intermediate JavaScript cache.
23 *
24 * @type Object
25 */
26
27var cache = exports.cache = {};
28
29/**
30 * Self closing tags.
31 *
32 * @type Object
33 */
34
35var selfClosing = exports.selfClosing = [
36 'meta',
37 'img',
38 'link',
39 'br',
40 'hr',
41 'input',
42 'area',
43 'base'
44];
45
46/**
47 * Default supported doctypes.
48 *
49 * @type Object
50 */
51
52var doctypes = exports.doctypes = {
53 '5': '<!DOCTYPE html>',
54 'xml': '<?xml version="1.0" encoding="utf-8" ?>',
55 'default': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
56 'transitional': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
57 'strict': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
58 'frameset': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
59 '1.1': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
60 'basic': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">',
61 'mobile': '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">'
62};
63
64/**
65 * Filters.
66 *
67 * @type Object
68 */
69
70var filters = exports.filters = {
71
72 /**
73 * Wrap text with CDATA block.
74 */
75
76 cdata: function(str){
77 return '<![CDATA[\\n' + str + '\\n]]>';
78 },
79
80 /**
81 * Wrap text with script and CDATA tags.
82 */
83
84 javascript: function(str){
85 return '<script type="text/javascript">\\n//<![CDATA[\\n' + str + '\\n//]]></script>';
86 },
87
88 /**
89 * Transform sass to css, wrapped in style tags.
90 */
91
92 sass: function(str){
93 str = str.replace(/\\n/g, '\n');
94 var sass = require('sass').render(str).replace(/\n/g, '\\n');
95 return '<style>' + sass + '</style>';
96 },
97
98 /**
99 * Transform markdown to html.
100 */
101
102 markdown: function(str){
103 str = str.replace(/\\n/g, '\n');
104 return require('markdown').parse(str).replace(/\n/g, '\\n');
105 }
106};
107
108/**
109 * Initialize jade parser with the given input string.
110 *
111 * @param {String} str
112 * @param {String} filename
113 * @api public
114 */
115
116function Parser(str, filename){
117 this.input = str.replace(/\r\n|\r/g, '\n');
118 this.filename = filename;
119 this.deferredTokens = [];
120 this.lastIndents = 0;
121 this.lineno = 1;
122}
123
124/**
125 * Parser prototype.
126 */
127
128Parser.prototype = {
129
130 /**
131 * Output token stack for debugging.
132 *
133 * @api private
134 */
135
136 debug: function(){
137 var tok, width = 8;
138 while ((tok = this.advance).type !== 'eos') {
139 var type = tok.type,
140 pad = width - type.length;
141 while (pad--) type += ' ';
142 sys.puts(tok.line
143 + ' : \x1B[1m' + type + '\x1B[0m'
144 + ' ' + sys.inspect(tok.val)
145 + (tok.attrs ? ' ' + sys.inspect(tok.attrs) : ''));
146 }
147 },
148
149 /**
150 * Return the next token object.
151 *
152 * @return {Object}
153 * @api private
154 */
155
156 get advance(){
157 var self = this,
158 captures;
159
160 if (this.stash) {
161 var tok = this.stash;
162 delete this.stash;
163 return tok;
164 }
165
166 if (this.deferredTokens.length) {
167 return this.deferredTokens.shift();
168 }
169
170 /**
171 * Generate token object.
172 */
173
174 function token(type){
175 self.input = self.input.substr(captures[0].length);
176 return {
177 type: type,
178 line: self.lineno,
179 val: captures[1]
180 };
181 }
182
183 // EOS
184 if (!this.input.length) {
185 if (this.lastIndents-- > 0) {
186 return { type: 'outdent', line: this.lineno };
187 } else {
188 return { type: 'eos', line: this.lineno };
189 }
190 }
191
192 // Tag
193 if (captures = /^(\w[:\w]*)/.exec(this.input)) {
194 return token('tag');
195 }
196
197 // Filter
198 if (captures = /^:(\w+)/.exec(this.input)) {
199 return token('filter');
200 }
201
202 // Code
203 if (captures = /^(!?=|-)([^\n]+)/.exec(this.input)) {
204 var flags = captures[1];
205 captures[1] = captures[2];
206 var tok = token('code');
207 tok.escape = flags[0] === '=';
208 tok.buffer = flags[0] === '=' || flags[1] === '=';
209 return tok;
210 }
211
212 // Doctype
213 if (captures = /^!!! *(\w+)?/.exec(this.input)) {
214 return token('doctype');
215 }
216
217 // Id
218 if (captures = /^#([\w-]+)/.exec(this.input)) {
219 return token('id');
220 }
221
222 // Class
223 if (captures = /^\.([\w-]+)/.exec(this.input)) {
224 return token('class');
225 }
226
227 // Attributes
228 if (captures = /^\( *(.+) *\)/.exec(this.input)) {
229 var tok = token('attrs'),
230 attrs = tok.val.split(/ *, */);
231 tok.attrs = {};
232 for (var i = 0, len = attrs.length; i < len; ++i) {
233 var pair = attrs[i];
234
235 // Support = and :
236 var colon = pair.indexOf(':'),
237 equal = pair.indexOf('=');
238
239 // Boolean
240 if (colon < 0 && equal < 0) {
241 var key = pair,
242 val = true;
243 } else {
244 // Split on first = or :
245 var split = equal >= 0
246 ? equal
247 : colon;
248 if (colon >= 0 && colon < equal) split = colon;
249 var key = pair.substr(0, split),
250 val = pair.substr(++split, pair.length);
251 }
252 tok.attrs[key.trim()] = val;
253 }
254 return tok;
255 }
256
257 // Indent
258 if (captures = /^\n( *)/.exec(this.input)) {
259 ++this.lineno;
260 var tok = token('indent'),
261 indents = tok.val.length / 2;
262 if (this.input[0] === '\n') {
263 tok.type = 'newline';
264 return tok;
265 } else if (indents % 1 !== 0) {
266 throw new Error('Invalid indentation, got '
267 + tok.val.length + ' space'
268 + (tok.val.length > 1 ? 's' : '')
269 + ', must be a multiple of two.');
270 } else if (indents === this.lastIndents) {
271 tok.type = 'newline';
272 } else if (indents > this.lastIndents + 1) {
273 throw new Error('Invalid indentation, got '
274 + indents + ' expected '
275 + (this.lastIndents + 1) + '.');
276 } else if (indents < this.lastIndents) {
277 var n = this.lastIndents - indents;
278 tok.type = 'outdent';
279 while (--n) {
280 this.deferredTokens.push({
281 type: 'outdent',
282 line: this.lineno
283 });
284 }
285 }
286 this.lastIndents = indents;
287 return tok;
288 }
289
290 // Text
291 if (captures = /^(?:\| ?)?([^\n]+)/.exec(this.input)) {
292 return token('text');
293 }
294 },
295
296 /**
297 * Single token lookahead.
298 *
299 * @return {Object}
300 * @api private
301 */
302
303 get peek() {
304 return this.stash = this.advance;
305 },
306
307 /**
308 * Instrument template lineno.
309 *
310 * @return {String}
311 * @api private
312 */
313
314 get _() {
315 return '_.lineno = ' + this.lineno + ';';
316 },
317
318 /**
319 * Parse input returning a string of js for evaluation.
320 *
321 * @return {String}
322 * @api public
323 */
324
325 parse: function(){
326 var buf = ['var buf = [];'];
327 while (this.peek.type !== 'eos') {
328 buf.push(this.parseExpr());
329 }
330 buf.push("return buf.join('');");
331 return buf.join('\n');
332 },
333
334 /**
335 * Expect the given type, or throw an exception.
336 *
337 * @param {String} type
338 * @api private
339 */
340
341 expect: function(type){
342 if (this.peek.type === type) {
343 return this.advance;
344 } else {
345 throw new Error('expected "' + type + '", but got "' + this.peek.type + '"');
346 }
347 },
348
349 /**
350 * tag
351 * | id
352 * | class
353 * | text
354 * | filter
355 * | doctype
356 * | code block?
357 * | expr newline
358 */
359
360 parseExpr: function(){
361 switch (this.peek.type) {
362 case 'tag':
363 return this.parseTag();
364 case 'doctype':
365 return this.parseDoctype();
366 case 'filter':
367 return this.parseFilter();
368 case 'text':
369 return "buf.push('" + interpolate(this.advance.val.replace(/'/g, "\\'")) + " ');";
370 case 'id':
371 case 'class':
372 var tok = this.advance;
373 this.deferredTokens.push({
374 val: 'div',
375 type: 'tag',
376 line: this.lineno
377 });
378 this.deferredTokens.push(tok);
379 return this.parseExpr();
380 case 'code':
381 var tok = this.advance,
382 val = tok.val;
383 var buf = tok.buffer
384 ? 'buf.push(' + (tok.escape
385 ? 'escape(' + val + ')'
386 : val) + ')'
387 : val;
388 return this.peek.type === 'indent'
389 ? buf + '\n(function(){' + this.parseBlock() + '})();'
390 : buf + ';';
391 case 'newline':
392 this.advance;
393 return this._ + this.parseExpr();
394 }
395 },
396
397 /**
398 * doctype
399 */
400
401 parseDoctype: function(){
402 var name = this.expect('doctype').val;
403 if (name === '5') this.mode = 'html 5';
404 return "buf.push('" + doctypes[name || 'default'] + "');";
405 },
406
407 /**
408 * filter text
409 */
410
411 parseFilter: function(){
412 var name = this.expect('filter').val,
413 filter = filters[name];
414 if (filter) {
415 return "buf.push('" + filter(interpolate(this.parseTextBlock())) + "');";
416 } else {
417 throw new Error('unknown filter ":' + name + '"');
418 }
419 },
420
421 /**
422 * indent (text | newline)* outdent
423 */
424
425 parseTextBlock: function(){
426 var buf = [];
427 this.expect('indent');
428 while (this.peek.type === 'text' || this.peek.type === 'newline') {
429 if (this.peek.type === 'newline') {
430 this.advance;
431 buf.push('\\n');
432 } else {
433 buf.push(this.advance.val);
434 }
435 }
436 this.expect('outdent');
437 return buf.join('');
438 },
439
440 /**
441 * indent expr* outdent
442 */
443
444 parseBlock: function(){
445 var buf = [];
446 buf.push(this._); this.expect('indent');
447 while (this.peek.type !== 'outdent') {
448 buf.push(this.parseExpr());
449 }
450 this.expect('outdent');
451 return buf.join('\n');
452 },
453
454 /**
455 * tag (attrs | class | id)* (text | code | block)
456 */
457
458 parseTag: function(){
459 var name = this.advance.val,
460 html5 = this.mode === 'html 5',
461 hasAttrs = false,
462 attrBuf = '',
463 codeClass = '',
464 classes = [],
465 attrs = {},
466 buf = [];
467
468 // (attrs | class | id)*
469 out:
470 while (1) {
471 switch (this.peek.type) {
472 case 'id':
473 hasAttrs = true;
474 attrs.id = '"' + this.advance.val + '"';
475 continue;
476 case 'class':
477 hasAttrs = true;
478 classes.push(this.advance.val);
479 continue;
480 case 'attrs':
481 hasAttrs = true;
482 var obj = this.advance.attrs,
483 keys = Object.keys(obj);
484 for (var i = 0, len = keys.length; i < len; ++i) {
485 var key = keys[i],
486 val = obj[key];
487 if (key === 'class') {
488 codeClass = val;
489 } else {
490 attrs[key] = val === undefined
491 ? true
492 : val;
493 attrs.html5 = html5;
494 }
495 }
496 continue;
497 default:
498 break out;
499 }
500 }
501
502 // (text | code | block)
503 switch (this.peek.type) {
504 case 'text':
505 buf.push("buf.push('" + interpolate(this.advance.val.trim().replace(/'/g, "\\'")) + "');");
506 break;
507 case 'code':
508 var tok = this.advance;
509 if (tok.buffer) {
510 buf.push('buf.push(' + (tok.escape
511 ? 'escape(' + tok.val + ')'
512 : tok.val) + ');');
513 } else {
514 buf.push(tok.val + ';');
515 }
516 break;
517 case 'indent':
518 buf.push(this.parseBlock());
519 break;
520 }
521
522 // Build attrs
523 if (hasAttrs) {
524 // Classes
525 if (classes.length) {
526 attrs['class'] = '"' + classes.join(' ') + '"';
527 }
528 if (codeClass) {
529 if (attrs['class']) {
530 attrs['class'] += ' + " " + (' + codeClass + ')';
531 } else {
532 attrs['class'] = codeClass;
533 }
534 }
535
536 // Attributes
537 attrBuf += "' + attrs({ ";
538 var keys = Object.keys(attrs);
539 for (var i = 0, len = keys.length; i < len; ++i) {
540 var key = keys[i],
541 val = attrs[key];
542 attrBuf += "'" + key + "': " + val + (i === len - 1 ? '' : ', ');
543 }
544 attrBuf += " }) + '";
545 } else {
546 attrBuf = "' + '";
547 }
548
549 // Build the tag
550 if (selfClosing.indexOf(name) >= 0) {
551 return [
552 "buf.push('<" + name + attrBuf + (html5 ? '' : ' /' ) + ">');",
553 buf.join('\n')
554 ].join('\n');
555 } else {
556 return [
557 "buf.push('<" + name + attrBuf + ">');",
558 buf.join('\n'),
559 "buf.push('</" + name + ">');"
560 ].join('\n');
561 }
562 }
563};
564
565/**
566 * Render the given attributes object.
567 *
568 * @param {Object} obj
569 * @return {String}
570 * @api private
571 */
572
573function attrs(obj){
574 var buf = [],
575 html5 = obj.html5;
576 delete obj.html5;
577 var keys = Object.keys(obj);
578 len = keys.length;
579 if (len) {
580 buf.push('');
581 for (var i = 0; i < len; ++i) {
582 var key = keys[i],
583 val = obj[key];
584 if (typeof val === 'boolean' || val === '' || val == null) {
585 if (val) {
586 html5
587 ? buf.push(key)
588 : buf.push(key + '="' + key + '"');
589 }
590 } else {
591 buf.push(key + '="' + escape(val) + '"');
592 }
593 }
594 }
595 return buf.join(' ');
596}
597
598/**
599 * Escape the given string of `html`.
600 *
601 * @param {String} html
602 * @return {String}
603 * @api private
604 */
605
606function escape(html){
607 return String(html)
608 .replace(/&(?!\w+;)/g, '&amp;')
609 .replace(/</g, '&lt;')
610 .replace(/>/g, '&gt;')
611 .replace(/"/g, '&quot;');
612}
613
614/**
615 * Convert interpolation in the given string to JavaScript.
616 *
617 * @param {String} str
618 * @return {String}
619 * @api private
620 */
621
622function interpolate(str){
623 return str.replace(/(\\)?#{(.*?)}/g, function(str, escape, code){
624 return escape
625 ? str
626 : "' + (" + code + ") + '";
627 });
628}
629
630/**
631 * Render the given `str` of jade.
632 *
633 * Options:
634 *
635 * - `scope` Evaluation scope (`this`). Also referred to as `context`
636 * - `locals` Local variable object
637 * - `filename` Used in exceptions, and required by `cache`
638 * - `cache` Cache intermediate JavaScript in memory keyed by `filename`
639 *
640 * @param {String} str
641 * @param {Object} options
642 * @return {String}
643 * @api public
644 */
645
646exports.render = function(str, options){
647 var js,
648 options = options || {},
649 filename = options.filename;
650
651 // Attempt to parse
652 function parse(){
653 try {
654 var parser = new Parser(str, filename);
655 if (options.debug) {
656 parser.debug();
657 parser = new Parser(str, filename);
658 }
659 var js = parser.parse()
660 if (options.debug) sys.puts('\nfunction:', js.replace(/^/gm, ' '));
661 return js;
662 } catch (err) {
663 rethrow(err, parser.lineno);
664 }
665 }
666
667 // Re-throw the given error
668 function rethrow(err, lineno){
669 var start = lineno - 3 > 0
670 ? lineno - 3
671 : 0;
672 // Error context
673 var context = str.split('\n').slice(start, lineno).map(function(line, i){
674 return ' ' + (i + start + 1) + '. ' + sys.inspect(line);
675 }).join('\n');
676
677 // Alter exception message
678 err.path = filename;
679 err.message = (filename || 'Jade') + ':' + lineno
680 + '\n' + context + '\n\n' + err.message;
681 throw err;
682 }
683
684 // Cache templates
685 if (options.cache) {
686 if (filename) {
687 if (cache[filename]) {
688 js = cache[filename];
689 } else {
690 js = cache[filename] = parse();
691 }
692 } else {
693 throw new Error('filename is required when using the cache option');
694 }
695 } else {
696 js = parse();
697 }
698
699 // Generate function
700 try {
701 var fn = Function('locals, attrs, escape, _', 'with (locals) {' + js + '}');
702 } catch (err) {
703 // TODO: re-catch and apply real lineno
704 process.compile(js, filename || 'Jade');
705 return;
706 }
707
708 try {
709 var _ = { lineno: 1 };
710 return fn.call(options.scope || options.context,
711 options.locals || {},
712 attrs,
713 escape,
714 _);
715 } catch (err) {
716 rethrow(err, _.lineno);
717 }
718};
719
720/**
721 * Render jade template at the given `path`.
722 *
723 * @param {String} path
724 * @param {Object} options
725 * @param {Function} fn
726 * @api public
727 */
728
729exports.renderFile = function(path, options, fn){
730 if (typeof options === 'function') {
731 fn = options;
732 options = {};
733 }
734 options.filename = path;
735 fs.readFile(path, 'utf8', function(err, str){
736 if (err) {
737 fn(err);
738 } else {
739 try {
740 fn(null, exports.render(str, options));
741 } catch (err) {
742 fn(err);
743 }
744 }
745 });
746};
\No newline at end of file