UNPKG

10.4 kBJavaScriptView Raw
1
2/*!
3 * Jade - Compiler
4 * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
5 * MIT Licensed
6 */
7
8/**
9 * Module dependencies.
10 */
11
12var nodes = require('./nodes')
13 , filters = require('./filters')
14 , doctypes = require('./doctypes')
15 , selfClosing = require('./self-closing')
16 , inlineTags = require('./inline-tags')
17 , utils = require('./utils');
18
19// if browser
20//
21// if (!Object.keys) {
22// Object.keys = function(obj){
23// var arr = [];
24// for (var key in obj) {
25// if (obj.hasOwnProperty(key)) {
26// arr.push(obj);
27// }
28// }
29// return arr;
30// }
31// }
32//
33// if (!String.prototype.trimLeft) {
34// String.prototype.trimLeft = function(){
35// return this.replace(/^\s+/, '');
36// }
37// }
38//
39// end
40
41
42/**
43 * Initialize `Compiler` with the given `node`.
44 *
45 * @param {Node} node
46 * @param {Object} options
47 * @api public
48 */
49
50var Compiler = module.exports = function Compiler(node, options) {
51 this.options = options = options || {};
52 this.node = node;
53 this.hasCompiledDoctype = false;
54 this.hasCompiledTag = false;
55 this.pp = options.pretty || false;
56 this.debug = false !== options.compileDebug;
57 this.indents = 0;
58 if (options.doctype) this.setDoctype(options.doctype);
59};
60
61/**
62 * Compiler prototype.
63 */
64
65Compiler.prototype = {
66
67 /**
68 * Compile parse tree to JavaScript.
69 *
70 * @api public
71 */
72
73 compile: function(){
74 this.buf = ['var interp;'];
75 this.lastBufferedIdx = -1
76 this.visit(this.node);
77 return this.buf.join('\n');
78 },
79
80 /**
81 * Sets the default doctype `name`. Sets terse mode to `true` when
82 * html 5 is used, causing self-closing tags to end with ">" vs "/>",
83 * and boolean attributes are not mirrored.
84 *
85 * @param {string} name
86 * @api public
87 */
88
89 setDoctype: function(name){
90 var doctype = doctypes[(name || 'default').toLowerCase()];
91 if (!doctype) throw new Error('unknown doctype "' + name + '"');
92 this.doctype = doctype;
93 this.terse = '5' == name || 'html' == name;
94 this.xml = 0 == this.doctype.indexOf('<?xml');
95 },
96
97 /**
98 * Buffer the given `str` optionally escaped.
99 *
100 * @param {String} str
101 * @param {Boolean} esc
102 * @api public
103 */
104
105 buffer: function(str, esc){
106 if (esc) str = utils.escape(str);
107
108 if (this.lastBufferedIdx == this.buf.length) {
109 this.lastBuffered += str;
110 this.buf[this.lastBufferedIdx - 1] = "buf.push('" + this.lastBuffered + "');"
111 } else {
112 this.buf.push("buf.push('" + str + "');");
113 this.lastBuffered = str;
114 this.lastBufferedIdx = this.buf.length;
115 }
116 },
117
118 /**
119 * Buffer the given `node`'s lineno.
120 *
121 * @param {Node} node
122 * @api public
123 */
124
125 line: function(node){
126 if (false === node.instrumentLineNumber) return;
127 this.buf.push('__.lineno = ' + node.line + ';');
128 },
129
130 /**
131 * Visit `node`.
132 *
133 * @param {Node} node
134 * @api public
135 */
136
137 visit: function(node){
138 if (this.debug) this.line(node);
139 return this.visitNode(node);
140 },
141
142 /**
143 * Visit `node`.
144 *
145 * @param {Node} node
146 * @api public
147 */
148
149 visitNode: function(node){
150 var name = node.constructor.name
151 || node.constructor.toString().match(/function ([^(\s]+)()/)[1];
152 return this['visit' + name](node);
153 },
154
155 /**
156 * Visit all nodes in `block`.
157 *
158 * @param {Block} block
159 * @api public
160 */
161
162 visitBlock: function(block){
163 var len = len = block.nodes.length;
164 for (var i = 0; i < len; ++i) {
165 this.visit(block.nodes[i]);
166 }
167 },
168
169 /**
170 * Visit `doctype`. Sets terse mode to `true` when html 5
171 * is used, causing self-closing tags to end with ">" vs "/>",
172 * and boolean attributes are not mirrored.
173 *
174 * @param {Doctype} doctype
175 * @api public
176 */
177
178 visitDoctype: function(doctype){
179 if (doctype && (doctype.val || !this.doctype)) {
180 this.setDoctype(doctype.val || 'default');
181 }
182
183 if (this.doctype) this.buffer(this.doctype);
184 this.hasCompiledDoctype = true;
185 },
186
187 /**
188 * Visit `mixin`, generating a function that
189 * may be called within the template.
190 *
191 * @param {Mixin} mixin
192 * @api public
193 */
194
195 visitMixin: function(mixin){
196 var name = mixin.name.replace(/-/g, '_') + '_mixin'
197 , args = mixin.args || '';
198
199 if (mixin.block) {
200 this.buf.push('var ' + name + ' = function(' + args + '){');
201 this.visit(mixin.block);
202 this.buf.push('}');
203 } else {
204 this.buf.push(name + '(' + args + ');');
205 }
206 },
207
208 /**
209 * Visit `tag` buffering tag markup, generating
210 * attributes, visiting the `tag`'s code and block.
211 *
212 * @param {Tag} tag
213 * @api public
214 */
215
216 visitTag: function(tag){
217 this.indents++;
218 var name = tag.name;
219
220 if (!this.hasCompiledTag) {
221 if (!this.hasCompiledDoctype && 'html' == name) {
222 this.visitDoctype();
223 }
224 this.hasCompiledTag = true;
225 }
226
227 // pretty print
228 if (this.pp && inlineTags.indexOf(name) == -1) {
229 this.buffer('\\n' + Array(this.indents).join(' '));
230 }
231
232 if (~selfClosing.indexOf(name) && !this.xml) {
233 this.buffer('<' + name);
234 this.visitAttributes(tag.attrs);
235 this.terse
236 ? this.buffer('>')
237 : this.buffer('/>');
238 } else {
239 // Optimize attributes buffering
240 if (tag.attrs.length) {
241 this.buffer('<' + name);
242 if (tag.attrs.length) this.visitAttributes(tag.attrs);
243 this.buffer('>');
244 } else {
245 this.buffer('<' + name + '>');
246 }
247 if (tag.code) this.visitCode(tag.code);
248 if (tag.text) this.buffer(utils.text(tag.text.nodes[0].trimLeft()));
249 this.escape = 'pre' == tag.name;
250 this.visit(tag.block);
251
252 // pretty print
253 if (this.pp && !~inlineTags.indexOf(name) && !tag.textOnly) {
254 this.buffer('\\n' + Array(this.indents).join(' '));
255 }
256
257 this.buffer('</' + name + '>');
258 }
259 this.indents--;
260 },
261
262 /**
263 * Visit `filter`, throwing when the filter does not exist.
264 *
265 * @param {Filter} filter
266 * @api public
267 */
268
269 visitFilter: function(filter){
270 var fn = filters[filter.name];
271
272 // unknown filter
273 if (!fn) {
274 if (filter.isASTFilter) {
275 throw new Error('unknown ast filter "' + filter.name + ':"');
276 } else {
277 throw new Error('unknown filter ":' + filter.name + '"');
278 }
279 }
280 if (filter.isASTFilter) {
281 this.buf.push(fn(filter.block, this, filter.attrs));
282 } else {
283 var text = filter.block.nodes.join('');
284 this.buffer(utils.text(fn(text, filter.attrs)));
285 }
286 },
287
288 /**
289 * Visit `text` node.
290 *
291 * @param {Text} text
292 * @api public
293 */
294
295 visitText: function(text){
296 text = utils.text(text.nodes.join(''));
297 if (this.escape) text = escape(text);
298 this.buffer(text);
299 this.buffer('\\n');
300 },
301
302 /**
303 * Visit a `comment`, only buffering when the buffer flag is set.
304 *
305 * @param {Comment} comment
306 * @api public
307 */
308
309 visitComment: function(comment){
310 if (!comment.buffer) return;
311 if (this.pp) this.buffer('\\n' + Array(this.indents + 1).join(' '));
312 this.buffer('<!--' + utils.escape(comment.val) + '-->');
313 },
314
315 /**
316 * Visit a `BlockComment`.
317 *
318 * @param {Comment} comment
319 * @api public
320 */
321
322 visitBlockComment: function(comment){
323 if (!comment.buffer) return;
324 if (0 == comment.val.indexOf('if')) {
325 this.buffer('<!--[' + comment.val + ']>');
326 this.visit(comment.block);
327 this.buffer('<![endif]-->');
328 } else {
329 this.buffer('<!--' + comment.val);
330 this.visit(comment.block);
331 this.buffer('-->');
332 }
333 },
334
335 /**
336 * Visit `code`, respecting buffer / escape flags.
337 * If the code is followed by a block, wrap it in
338 * a self-calling function.
339 *
340 * @param {Code} code
341 * @api public
342 */
343
344 visitCode: function(code){
345 // Wrap code blocks with {}.
346 // we only wrap unbuffered code blocks ATM
347 // since they are usually flow control
348
349 // Buffer code
350 if (code.buffer) {
351 var val = code.val.trimLeft();
352 this.buf.push('var __val__ = ' + val);
353 val = 'null == __val__ ? "" : __val__';
354 if (code.escape) val = 'escape(' + val + ')';
355 this.buf.push("buf.push(" + val + ");");
356 } else {
357 this.buf.push(code.val);
358 }
359
360 // Block support
361 if (code.block) {
362 if (!code.buffer) this.buf.push('{');
363 this.visit(code.block);
364 if (!code.buffer) this.buf.push('}');
365 }
366 },
367
368 /**
369 * Visit `each` block.
370 *
371 * @param {Each} each
372 * @api public
373 */
374
375 visitEach: function(each){
376 this.buf.push(''
377 + '// iterate ' + each.obj + '\n'
378 + '(function(){\n'
379 + ' if (\'number\' == typeof ' + each.obj + '.length) {\n'
380 + ' for (var ' + each.key + ' = 0, $$l = ' + each.obj + '.length; ' + each.key + ' < $$l; ' + each.key + '++) {\n'
381 + ' var ' + each.val + ' = ' + each.obj + '[' + each.key + '];\n');
382
383 this.visit(each.block);
384
385 this.buf.push(''
386 + ' }\n'
387 + ' } else {\n'
388 + ' for (var ' + each.key + ' in ' + each.obj + ') {\n'
389 // if browser
390 // + ' if (' + each.obj + '.hasOwnProperty(' + each.key + ')){'
391 // end
392 + ' var ' + each.val + ' = ' + each.obj + '[' + each.key + '];\n');
393
394 this.visit(each.block);
395
396 // if browser
397 // this.buf.push(' }\n');
398 // end
399
400 this.buf.push(' }\n }\n}).call(this);\n');
401 },
402
403 /**
404 * Visit `attrs`.
405 *
406 * @param {Array} attrs
407 * @api public
408 */
409
410 visitAttributes: function(attrs){
411 var buf = []
412 , classes = [];
413
414 if (this.terse) buf.push('terse: true');
415
416 attrs.forEach(function(attr){
417 if (attr.name == 'class') {
418 classes.push('(' + attr.val + ')');
419 } else {
420 var pair = "'" + attr.name + "':(" + attr.val + ')';
421 buf.push(pair);
422 }
423 });
424
425 if (classes.length) {
426 classes = classes.join(" + ' ' + ");
427 buf.push("class: " + classes);
428 }
429
430 buf = buf.join(', ').replace('class:', '"class":');
431
432 this.buf.push("buf.push(attrs({ " + buf + " }));");
433 }
434};
435
436/**
437 * Escape the given string of `html`.
438 *
439 * @param {String} html
440 * @return {String}
441 * @api private
442 */
443
444function escape(html){
445 return String(html)
446 .replace(/&(?!\w+;)/g, '&amp;')
447 .replace(/</g, '&lt;')
448 .replace(/>/g, '&gt;')
449 .replace(/"/g, '&quot;');
450}