1 | 'use strict';
|
2 |
|
3 | var nodes = require('./nodes');
|
4 | var filters = require('./filters');
|
5 | var doctypes = require('./doctypes');
|
6 | var runtime = require('./runtime');
|
7 | var utils = require('./utils');
|
8 | var selfClosing = require('void-elements');
|
9 | var parseJSExpression = require('character-parser').parseMax;
|
10 | var constantinople = require('constantinople');
|
11 |
|
12 | function isConstant(src) {
|
13 | return constantinople(src, {jade: runtime, 'jade_interp': undefined});
|
14 | }
|
15 | function toConstant(src) {
|
16 | return constantinople.toConstant(src, {jade: runtime, 'jade_interp': undefined});
|
17 | }
|
18 | function errorAtNode(node, error) {
|
19 | error.line = node.line;
|
20 | error.filename = node.filename;
|
21 | return error;
|
22 | }
|
23 |
|
24 |
|
25 |
|
26 |
|
27 |
|
28 |
|
29 |
|
30 |
|
31 |
|
32 | var 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 |
|
52 |
|
53 |
|
54 | Compiler.prototype = {
|
55 |
|
56 | |
57 |
|
58 |
|
59 |
|
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 |
|
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 |
|
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
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 |
|
101 |
|
102 |
|
103 |
|
104 |
|
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]) {
|
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 |
|
147 |
|
148 |
|
149 |
|
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 |
|
172 |
|
173 |
|
174 |
|
175 |
|
176 |
|
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 |
|
189 |
|
190 |
|
191 |
|
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 |
|
206 |
|
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 |
|
219 |
|
220 |
|
221 |
|
222 |
|
223 |
|
224 | visitNode: function(node){
|
225 | return this['visit' + node.type](node);
|
226 | },
|
227 |
|
228 | |
229 |
|
230 |
|
231 |
|
232 |
|
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 |
|
246 |
|
247 |
|
248 |
|
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 |
|
265 |
|
266 |
|
267 |
|
268 |
|
269 |
|
270 | visitLiteral: function(node){
|
271 | this.buffer(node.str);
|
272 | },
|
273 |
|
274 | |
275 |
|
276 |
|
277 |
|
278 |
|
279 |
|
280 |
|
281 | visitBlock: function(block){
|
282 | var len = block.nodes.length
|
283 | , escape = this.escape
|
284 | , pp = this.pp
|
285 |
|
286 |
|
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 |
|
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 |
|
297 | if (block.nodes[i+1] && block.nodes[i].isText && block.nodes[i+1].isText)
|
298 | this.buffer('\n');
|
299 | }
|
300 | },
|
301 |
|
302 | |
303 |
|
304 |
|
305 |
|
306 |
|
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 |
|
317 |
|
318 |
|
319 |
|
320 |
|
321 |
|
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 |
|
335 |
|
336 |
|
337 |
|
338 |
|
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 |
|
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 |
|
408 |
|
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 |
|
428 |
|
429 |
|
430 |
|
431 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
498 |
|
499 |
|
500 |
|
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 |
|
517 |
|
518 |
|
519 |
|
520 |
|
521 |
|
522 | visitText: function(text){
|
523 | this.buffer(text.val, true);
|
524 | },
|
525 |
|
526 | |
527 |
|
528 |
|
529 |
|
530 |
|
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 |
|
541 |
|
542 |
|
543 |
|
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 |
|
557 |
|
558 |
|
559 |
|
560 |
|
561 |
|
562 |
|
563 |
|
564 | visitCode: function(code){
|
565 |
|
566 |
|
567 |
|
568 |
|
569 |
|
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 |
|
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 |
|
589 |
|
590 |
|
591 |
|
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 |
|
639 |
|
640 |
|
641 |
|
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 |
|
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 | };
|