UNPKG

19.8 kBJavaScriptView Raw
1'use strict';
2
3var nodes = require('./nodes');
4var filters = require('./filters');
5var doctypes = require('./doctypes');
6var runtime = require('./runtime');
7var utils = require('./utils');
8var selfClosing = require('void-elements');
9var parseJSExpression = require('character-parser').parseMax;
10var constantinople = require('constantinople');
11
12function isConstant(src) {
13 return constantinople(src, {jade: runtime, 'jade_interp': undefined});
14}
15function toConstant(src) {
16 return constantinople.toConstant(src, {jade: runtime, 'jade_interp': undefined});
17}
18function errorAtNode(node, error) {
19 error.line = node.line;
20 error.filename = node.filename;
21 return error;
22}
23
24/**
25 * Initialize `Compiler` with the given `node`.
26 *
27 * @param {Node} node
28 * @param {Object} options
29 * @api public
30 */
31
32var Compiler = module.exports = function Compiler(node, options) {
33 this.options = options = options || {};
34 this.node = node;
35 this.hasCompiledDoctype = false;
36 this.hasCompiledTag = false;
37 this.pp = options.pretty || false;
38 if (this.pp && typeof this.pp !== 'string') {
39 this.pp = ' ';
40 }
41 this.debug = false !== options.compileDebug;
42 this.indents = 0;
43 this.parentIndents = 0;
44 this.terse = false;
45 this.mixins = {};
46 this.dynamicMixins = false;
47 if (options.doctype) this.setDoctype(options.doctype);
48};
49
50/**
51 * Compiler prototype.
52 */
53
54Compiler.prototype = {
55
56 /**
57 * Compile parse tree to JavaScript.
58 *
59 * @api public
60 */
61
62 compile: function(){
63 this.buf = [];
64 if (this.pp) this.buf.push("var jade_indent = [];");
65 this.lastBufferedIdx = -1;
66 this.visit(this.node);
67 if (!this.dynamicMixins) {
68 // if there are no dynamic mixins we can remove any un-used mixins
69 var mixinNames = Object.keys(this.mixins);
70 for (var i = 0; i < mixinNames.length; i++) {
71 var mixin = this.mixins[mixinNames[i]];
72 if (!mixin.used) {
73 for (var x = 0; x < mixin.instances.length; x++) {
74 for (var y = mixin.instances[x].start; y < mixin.instances[x].end; y++) {
75 this.buf[y] = '';
76 }
77 }
78 }
79 }
80 }
81 return this.buf.join('\n');
82 },
83
84 /**
85 * Sets the default doctype `name`. Sets terse mode to `true` when
86 * html 5 is used, causing self-closing tags to end with ">" vs "/>",
87 * and boolean attributes are not mirrored.
88 *
89 * @param {string} name
90 * @api public
91 */
92
93 setDoctype: function(name){
94 this.doctype = doctypes[name.toLowerCase()] || '<!DOCTYPE ' + name + '>';
95 this.terse = this.doctype.toLowerCase() == '<!doctype html>';
96 this.xml = 0 == this.doctype.indexOf('<?xml');
97 },
98
99 /**
100 * Buffer the given `str` exactly as is or with interpolation
101 *
102 * @param {String} str
103 * @param {Boolean} interpolate
104 * @api public
105 */
106
107 buffer: function (str, interpolate) {
108 var self = this;
109 if (interpolate) {
110 var match = /(\\)?([#!]){((?:.|\n)*)$/.exec(str);
111 if (match) {
112 this.buffer(str.substr(0, match.index), false);
113 if (match[1]) { // escape
114 this.buffer(match[2] + '{', false);
115 this.buffer(match[3], true);
116 return;
117 } else {
118 var rest = match[3];
119 var range = parseJSExpression(rest);
120 var code = ('!' == match[2] ? '' : 'jade.escape') + "((jade_interp = " + range.src + ") == null ? '' : jade_interp)";
121 this.bufferExpression(code);
122 this.buffer(rest.substr(range.end + 1), true);
123 return;
124 }
125 }
126 }
127
128 str = utils.stringify(str);
129 str = str.substr(1, str.length - 2);
130
131 if (this.lastBufferedIdx == this.buf.length) {
132 if (this.lastBufferedType === 'code') this.lastBuffered += ' + "';
133 this.lastBufferedType = 'text';
134 this.lastBuffered += str;
135 this.buf[this.lastBufferedIdx - 1] = 'buf.push(' + this.bufferStartChar + this.lastBuffered + '");'
136 } else {
137 this.buf.push('buf.push("' + str + '");');
138 this.lastBufferedType = 'text';
139 this.bufferStartChar = '"';
140 this.lastBuffered = str;
141 this.lastBufferedIdx = this.buf.length;
142 }
143 },
144
145 /**
146 * Buffer the given `src` so it is evaluated at run time
147 *
148 * @param {String} src
149 * @api public
150 */
151
152 bufferExpression: function (src) {
153 if (isConstant(src)) {
154 return this.buffer(toConstant(src) + '', false)
155 }
156 if (this.lastBufferedIdx == this.buf.length) {
157 if (this.lastBufferedType === 'text') this.lastBuffered += '"';
158 this.lastBufferedType = 'code';
159 this.lastBuffered += ' + (' + src + ')';
160 this.buf[this.lastBufferedIdx - 1] = 'buf.push(' + this.bufferStartChar + this.lastBuffered + ');'
161 } else {
162 this.buf.push('buf.push(' + src + ');');
163 this.lastBufferedType = 'code';
164 this.bufferStartChar = '';
165 this.lastBuffered = '(' + src + ')';
166 this.lastBufferedIdx = this.buf.length;
167 }
168 },
169
170 /**
171 * Buffer an indent based on the current `indent`
172 * property and an additional `offset`.
173 *
174 * @param {Number} offset
175 * @param {Boolean} newline
176 * @api public
177 */
178
179 prettyIndent: function(offset, newline){
180 offset = offset || 0;
181 newline = newline ? '\n' : '';
182 this.buffer(newline + Array(this.indents + offset).join(this.pp));
183 if (this.parentIndents)
184 this.buf.push("buf.push.apply(buf, jade_indent);");
185 },
186
187 /**
188 * Visit `node`.
189 *
190 * @param {Node} node
191 * @api public
192 */
193
194 visit: function(node){
195 var debug = this.debug;
196
197 if (debug) {
198 this.buf.push('jade_debug.unshift(new jade.DebugItem( ' + node.line
199 + ', ' + (node.filename
200 ? utils.stringify(node.filename)
201 : 'jade_debug[0].filename')
202 + ' ));');
203 }
204
205 // Massive hack to fix our context
206 // stack for - else[ if] etc
207 if (false === node.debug && this.debug) {
208 this.buf.pop();
209 this.buf.pop();
210 }
211
212 this.visitNode(node);
213
214 if (debug) this.buf.push('jade_debug.shift();');
215 },
216
217 /**
218 * Visit `node`.
219 *
220 * @param {Node} node
221 * @api public
222 */
223
224 visitNode: function(node){
225 return this['visit' + node.type](node);
226 },
227
228 /**
229 * Visit case `node`.
230 *
231 * @param {Literal} node
232 * @api public
233 */
234
235 visitCase: function(node){
236 var _ = this.withinCase;
237 this.withinCase = true;
238 this.buf.push('switch (' + node.expr + '){');
239 this.visit(node.block);
240 this.buf.push('}');
241 this.withinCase = _;
242 },
243
244 /**
245 * Visit when `node`.
246 *
247 * @param {Literal} node
248 * @api public
249 */
250
251 visitWhen: function(node){
252 if ('default' == node.expr) {
253 this.buf.push('default:');
254 } else {
255 this.buf.push('case ' + node.expr + ':');
256 }
257 if (node.block) {
258 this.visit(node.block);
259 this.buf.push(' break;');
260 }
261 },
262
263 /**
264 * Visit literal `node`.
265 *
266 * @param {Literal} node
267 * @api public
268 */
269
270 visitLiteral: function(node){
271 this.buffer(node.str);
272 },
273
274 /**
275 * Visit all nodes in `block`.
276 *
277 * @param {Block} block
278 * @api public
279 */
280
281 visitBlock: function(block){
282 var len = block.nodes.length
283 , escape = this.escape
284 , pp = this.pp
285
286 // Pretty print multi-line text
287 if (pp && len > 1 && !escape && block.nodes[0].isText && block.nodes[1].isText)
288 this.prettyIndent(1, true);
289
290 for (var i = 0; i < len; ++i) {
291 // Pretty print text
292 if (pp && i > 0 && !escape && block.nodes[i].isText && block.nodes[i-1].isText)
293 this.prettyIndent(1, false);
294
295 this.visit(block.nodes[i]);
296 // Multiple text nodes are separated by newlines
297 if (block.nodes[i+1] && block.nodes[i].isText && block.nodes[i+1].isText)
298 this.buffer('\n');
299 }
300 },
301
302 /**
303 * Visit a mixin's `block` keyword.
304 *
305 * @param {MixinBlock} block
306 * @api public
307 */
308
309 visitMixinBlock: function(block){
310 if (this.pp) this.buf.push("jade_indent.push('" + Array(this.indents + 1).join(this.pp) + "');");
311 this.buf.push('block && block();');
312 if (this.pp) this.buf.push("jade_indent.pop();");
313 },
314
315 /**
316 * Visit `doctype`. Sets terse mode to `true` when html 5
317 * is used, causing self-closing tags to end with ">" vs "/>",
318 * and boolean attributes are not mirrored.
319 *
320 * @param {Doctype} doctype
321 * @api public
322 */
323
324 visitDoctype: function(doctype){
325 if (doctype && (doctype.val || !this.doctype)) {
326 this.setDoctype(doctype.val || 'default');
327 }
328
329 if (this.doctype) this.buffer(this.doctype);
330 this.hasCompiledDoctype = true;
331 },
332
333 /**
334 * Visit `mixin`, generating a function that
335 * may be called within the template.
336 *
337 * @param {Mixin} mixin
338 * @api public
339 */
340
341 visitMixin: function(mixin){
342 var name = 'jade_mixins[';
343 var args = mixin.args || '';
344 var block = mixin.block;
345 var attrs = mixin.attrs;
346 var attrsBlocks = mixin.attributeBlocks.slice();
347 var pp = this.pp;
348 var dynamic = mixin.name[0]==='#';
349 var key = mixin.name;
350 if (dynamic) this.dynamicMixins = true;
351 name += (dynamic ? mixin.name.substr(2,mixin.name.length-3):'"'+mixin.name+'"')+']';
352
353 this.mixins[key] = this.mixins[key] || {used: false, instances: []};
354 if (mixin.call) {
355 this.mixins[key].used = true;
356 if (pp) this.buf.push("jade_indent.push('" + Array(this.indents + 1).join(pp) + "');")
357 if (block || attrs.length || attrsBlocks.length) {
358
359 this.buf.push(name + '.call({');
360
361 if (block) {
362 this.buf.push('block: function(){');
363
364 // Render block with no indents, dynamically added when rendered
365 this.parentIndents++;
366 var _indents = this.indents;
367 this.indents = 0;
368 this.visit(mixin.block);
369 this.indents = _indents;
370 this.parentIndents--;
371
372 if (attrs.length || attrsBlocks.length) {
373 this.buf.push('},');
374 } else {
375 this.buf.push('}');
376 }
377 }
378
379 if (attrsBlocks.length) {
380 if (attrs.length) {
381 var val = this.attrs(attrs);
382 attrsBlocks.unshift(val);
383 }
384 this.buf.push('attributes: jade.merge([' + attrsBlocks.join(',') + '])');
385 } else if (attrs.length) {
386 var val = this.attrs(attrs);
387 this.buf.push('attributes: ' + val);
388 }
389
390 if (args) {
391 this.buf.push('}, ' + args + ');');
392 } else {
393 this.buf.push('});');
394 }
395
396 } else {
397 this.buf.push(name + '(' + args + ');');
398 }
399 if (pp) this.buf.push("jade_indent.pop();")
400 } else {
401 var mixin_start = this.buf.length;
402 args = args ? args.split(',') : [];
403 var rest;
404 if (args.length && /^\.\.\./.test(args[args.length - 1].trim())) {
405 rest = args.pop().trim().replace(/^\.\.\./, '');
406 }
407 // we need use jade_interp here for v8: https://code.google.com/p/v8/issues/detail?id=4165
408 // once fixed, use this: this.buf.push(name + ' = function(' + args.join(',') + '){');
409 this.buf.push(name + ' = jade_interp = function(' + args.join(',') + '){');
410 this.buf.push('var block = (this && this.block), attributes = (this && this.attributes) || {};');
411 if (rest) {
412 this.buf.push('var ' + rest + ' = [];');
413 this.buf.push('for (jade_interp = ' + args.length + '; jade_interp < arguments.length; jade_interp++) {');
414 this.buf.push(' ' + rest + '.push(arguments[jade_interp]);');
415 this.buf.push('}');
416 }
417 this.parentIndents++;
418 this.visit(block);
419 this.parentIndents--;
420 this.buf.push('};');
421 var mixin_end = this.buf.length;
422 this.mixins[key].instances.push({start: mixin_start, end: mixin_end});
423 }
424 },
425
426 /**
427 * Visit `tag` buffering tag markup, generating
428 * attributes, visiting the `tag`'s code and block.
429 *
430 * @param {Tag} tag
431 * @api public
432 */
433
434 visitTag: function(tag){
435 this.indents++;
436 var name = tag.name
437 , pp = this.pp
438 , self = this;
439
440 function bufferName() {
441 if (tag.buffer) self.bufferExpression(name);
442 else self.buffer(name);
443 }
444
445 if ('pre' == tag.name) this.escape = true;
446
447 if (!this.hasCompiledTag) {
448 if (!this.hasCompiledDoctype && 'html' == name) {
449 this.visitDoctype();
450 }
451 this.hasCompiledTag = true;
452 }
453
454 // pretty print
455 if (pp && !tag.isInline())
456 this.prettyIndent(0, true);
457
458 if (tag.selfClosing || (!this.xml && selfClosing[tag.name])) {
459 this.buffer('<');
460 bufferName();
461 this.visitAttributes(tag.attrs, tag.attributeBlocks.slice());
462 this.terse
463 ? this.buffer('>')
464 : this.buffer('/>');
465 // if it is non-empty throw an error
466 if (tag.block &&
467 !(tag.block.type === 'Block' && tag.block.nodes.length === 0) &&
468 tag.block.nodes.some(function (tag) {
469 return tag.type !== 'Text' || !/^\s*$/.test(tag.val)
470 })) {
471 throw errorAtNode(tag, new Error(name + ' is self closing and should not have content.'));
472 }
473 } else {
474 // Optimize attributes buffering
475 this.buffer('<');
476 bufferName();
477 this.visitAttributes(tag.attrs, tag.attributeBlocks.slice());
478 this.buffer('>');
479 if (tag.code) this.visitCode(tag.code);
480 this.visit(tag.block);
481
482 // pretty print
483 if (pp && !tag.isInline() && 'pre' != tag.name && !tag.canInline())
484 this.prettyIndent(0, true);
485
486 this.buffer('</');
487 bufferName();
488 this.buffer('>');
489 }
490
491 if ('pre' == tag.name) this.escape = false;
492
493 this.indents--;
494 },
495
496 /**
497 * Visit `filter`, throwing when the filter does not exist.
498 *
499 * @param {Filter} filter
500 * @api public
501 */
502
503 visitFilter: function(filter){
504 var text = filter.block.nodes.map(
505 function(node){ return node.val; }
506 ).join('\n');
507 filter.attrs.filename = this.options.filename;
508 try {
509 this.buffer(filters(filter.name, text, filter.attrs), true);
510 } catch (err) {
511 throw errorAtNode(filter, err);
512 }
513 },
514
515 /**
516 * Visit `text` node.
517 *
518 * @param {Text} text
519 * @api public
520 */
521
522 visitText: function(text){
523 this.buffer(text.val, true);
524 },
525
526 /**
527 * Visit a `comment`, only buffering when the buffer flag is set.
528 *
529 * @param {Comment} comment
530 * @api public
531 */
532
533 visitComment: function(comment){
534 if (!comment.buffer) return;
535 if (this.pp) this.prettyIndent(1, true);
536 this.buffer('<!--' + comment.val + '-->');
537 },
538
539 /**
540 * Visit a `BlockComment`.
541 *
542 * @param {Comment} comment
543 * @api public
544 */
545
546 visitBlockComment: function(comment){
547 if (!comment.buffer) return;
548 if (this.pp) this.prettyIndent(1, true);
549 this.buffer('<!--' + comment.val);
550 this.visit(comment.block);
551 if (this.pp) this.prettyIndent(1, true);
552 this.buffer('-->');
553 },
554
555 /**
556 * Visit `code`, respecting buffer / escape flags.
557 * If the code is followed by a block, wrap it in
558 * a self-calling function.
559 *
560 * @param {Code} code
561 * @api public
562 */
563
564 visitCode: function(code){
565 // Wrap code blocks with {}.
566 // we only wrap unbuffered code blocks ATM
567 // since they are usually flow control
568
569 // Buffer code
570 if (code.buffer) {
571 var val = code.val.trim();
572 val = 'null == (jade_interp = '+val+') ? "" : jade_interp';
573 if (code.escape) val = 'jade.escape(' + val + ')';
574 this.bufferExpression(val);
575 } else {
576 this.buf.push(code.val);
577 }
578
579 // Block support
580 if (code.block) {
581 if (!code.buffer) this.buf.push('{');
582 this.visit(code.block);
583 if (!code.buffer) this.buf.push('}');
584 }
585 },
586
587 /**
588 * Visit `each` block.
589 *
590 * @param {Each} each
591 * @api public
592 */
593
594 visitEach: function(each){
595 this.buf.push(''
596 + '// iterate ' + each.obj + '\n'
597 + ';(function(){\n'
598 + ' var $$obj = ' + each.obj + ';\n'
599 + ' if (\'number\' == typeof $$obj.length) {\n');
600
601 if (each.alternative) {
602 this.buf.push(' if ($$obj.length) {');
603 }
604
605 this.buf.push(''
606 + ' for (var ' + each.key + ' = 0, $$l = $$obj.length; ' + each.key + ' < $$l; ' + each.key + '++) {\n'
607 + ' var ' + each.val + ' = $$obj[' + each.key + '];\n');
608
609 this.visit(each.block);
610
611 this.buf.push(' }\n');
612
613 if (each.alternative) {
614 this.buf.push(' } else {');
615 this.visit(each.alternative);
616 this.buf.push(' }');
617 }
618
619 this.buf.push(''
620 + ' } else {\n'
621 + ' var $$l = 0;\n'
622 + ' for (var ' + each.key + ' in $$obj) {\n'
623 + ' $$l++;'
624 + ' var ' + each.val + ' = $$obj[' + each.key + '];\n');
625
626 this.visit(each.block);
627
628 this.buf.push(' }\n');
629 if (each.alternative) {
630 this.buf.push(' if ($$l === 0) {');
631 this.visit(each.alternative);
632 this.buf.push(' }');
633 }
634 this.buf.push(' }\n}).call(this);\n');
635 },
636
637 /**
638 * Visit `attrs`.
639 *
640 * @param {Array} attrs
641 * @api public
642 */
643
644 visitAttributes: function(attrs, attributeBlocks){
645 if (attributeBlocks.length) {
646 if (attrs.length) {
647 var val = this.attrs(attrs);
648 attributeBlocks.unshift(val);
649 }
650 this.bufferExpression('jade.attrs(jade.merge([' + attributeBlocks.join(',') + ']), ' + utils.stringify(this.terse) + ')');
651 } else if (attrs.length) {
652 this.attrs(attrs, true);
653 }
654 },
655
656 /**
657 * Compile attributes.
658 */
659
660 attrs: function(attrs, buffer){
661 var buf = [];
662 var classes = [];
663 var classEscaping = [];
664
665 attrs.forEach(function(attr){
666 var key = attr.name;
667 var escaped = attr.escaped;
668
669 if (key === 'class') {
670 classes.push(attr.val);
671 classEscaping.push(attr.escaped);
672 } else if (isConstant(attr.val)) {
673 if (buffer) {
674 this.buffer(runtime.attr(key, toConstant(attr.val), escaped, this.terse));
675 } else {
676 var val = toConstant(attr.val);
677 if (key === 'style') val = runtime.style(val);
678 if (escaped && !(key.indexOf('data') === 0 && typeof val !== 'string')) {
679 val = runtime.escape(val);
680 }
681 buf.push(utils.stringify(key) + ': ' + utils.stringify(val));
682 }
683 } else {
684 if (buffer) {
685 this.bufferExpression('jade.attr("' + key + '", ' + attr.val + ', ' + utils.stringify(escaped) + ', ' + utils.stringify(this.terse) + ')');
686 } else {
687 var val = attr.val;
688 if (key === 'style') {
689 val = 'jade.style(' + val + ')';
690 }
691 if (escaped && !(key.indexOf('data') === 0)) {
692 val = 'jade.escape(' + val + ')';
693 } else if (escaped) {
694 val = '(typeof (jade_interp = ' + val + ') == "string" ? jade.escape(jade_interp) : jade_interp)';
695 }
696 buf.push(utils.stringify(key) + ': ' + val);
697 }
698 }
699 }.bind(this));
700 if (buffer) {
701 if (classes.every(isConstant)) {
702 this.buffer(runtime.cls(classes.map(toConstant), classEscaping));
703 } else {
704 this.bufferExpression('jade.cls([' + classes.join(',') + '], ' + utils.stringify(classEscaping) + ')');
705 }
706 } else if (classes.length) {
707 if (classes.every(isConstant)) {
708 classes = utils.stringify(runtime.joinClasses(classes.map(toConstant).map(runtime.joinClasses).map(function (cls, i) {
709 return classEscaping[i] ? runtime.escape(cls) : cls;
710 })));
711 } else {
712 classes = '(jade_interp = ' + utils.stringify(classEscaping) + ',' +
713 ' jade.joinClasses([' + classes.join(',') + '].map(jade.joinClasses).map(function (cls, i) {' +
714 ' return jade_interp[i] ? jade.escape(cls) : cls' +
715 ' }))' +
716 ')';
717 }
718 if (classes.length)
719 buf.push('"class": ' + classes);
720 }
721 return '{' + buf.join(',') + '}';
722 }
723};