UNPKG

9.99 kBJavaScriptView Raw
1
2/*!
3 * Jade - Parser
4 * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
5 * MIT Licensed
6 */
7
8/**
9 * Module dependencies.
10 */
11
12var Lexer = require('./lexer')
13 , nodes = require('./nodes');
14
15/**
16 * Initialize `Parser` with the given input `str` and `filename`.
17 *
18 * @param {String} str
19 * @param {String} filename
20 * @param {Object} options
21 * @api public
22 */
23
24var Parser = exports = module.exports = function Parser(str, filename, options){
25 this.input = str;
26 this.lexer = new Lexer(str, options);
27 this.filename = filename;
28};
29
30/**
31 * Tags that may not contain tags.
32 */
33
34var textOnly = exports.textOnly = ['code', 'script', 'textarea', 'style', 'title'];
35
36/**
37 * Parser prototype.
38 */
39
40Parser.prototype = {
41
42 /**
43 * Output parse tree to stdout.
44 *
45 * @api public
46 */
47
48 debug: function(){
49 var lexer = new Lexer(this.input)
50 , tree = require('sys').inspect(this.parse(), false, 12, true);
51 console.log('\n\x1b[1mParse Tree\x1b[0m:\n');
52 console.log(tree);
53 this.lexer = lexer;
54 },
55
56 /**
57 * Return the next token object.
58 *
59 * @return {Object}
60 * @api private
61 */
62
63 advance: function(){
64 return this.lexer.advance();
65 },
66
67 /**
68 * Single token lookahead.
69 *
70 * @return {Object}
71 * @api private
72 */
73
74 peek: function() {
75 return this.lookahead(1);
76 },
77
78 /**
79 * Return lexer lineno.
80 *
81 * @return {Number}
82 * @api private
83 */
84
85 line: function() {
86 return this.lexer.lineno;
87 },
88
89 /**
90 * `n` token lookahead.
91 *
92 * @param {Number} n
93 * @return {Object}
94 * @api private
95 */
96
97 lookahead: function(n){
98 return this.lexer.lookahead(n);
99 },
100
101 /**
102 * Parse input returning a string of js for evaluation.
103 *
104 * @return {String}
105 * @api public
106 */
107
108 parse: function(){
109 var block = new nodes.Block;
110 block.line = this.line();
111 while ('eos' != this.peek().type) {
112 if ('newline' == this.peek().type) {
113 this.advance();
114 } else {
115 block.push(this.parseExpr());
116 }
117 }
118 return block;
119 },
120
121 /**
122 * Expect the given type, or throw an exception.
123 *
124 * @param {String} type
125 * @api private
126 */
127
128 expect: function(type){
129 if (this.peek().type === type) {
130 return this.advance();
131 } else {
132 throw new Error('expected "' + type + '", but got "' + this.peek().type + '"');
133 }
134 },
135
136 /**
137 * Accept the given `type`.
138 *
139 * @param {String} type
140 * @api private
141 */
142
143 accept: function(type){
144 if (this.peek().type === type) {
145 return this.advance();
146 }
147 },
148
149 /**
150 * tag
151 * | doctype
152 * | mixin
153 * | include
154 * | filter
155 * | comment
156 * | text
157 * | each
158 * | code
159 * | id
160 * | class
161 */
162
163 parseExpr: function(){
164 switch (this.peek().type) {
165 case 'tag':
166 return this.parseTag();
167 case 'mixin':
168 return this.parseMixin();
169 case 'include':
170 return this.parseInclude();
171 case 'doctype':
172 return this.parseDoctype();
173 case 'filter':
174 return this.parseFilter();
175 case 'comment':
176 return this.parseComment();
177 case 'text':
178 return this.parseText();
179 case 'each':
180 return this.parseEach();
181 case 'code':
182 return this.parseCode();
183 case 'id':
184 case 'class':
185 var tok = this.advance();
186 this.lexer.defer(this.lexer.tok('tag', 'div'));
187 this.lexer.defer(tok);
188 return this.parseExpr();
189 default:
190 throw new Error('unexpected token "' + this.peek().type + '"');
191 }
192 },
193
194 /**
195 * Text
196 */
197
198 parseText: function(){
199 var tok = this.expect('text')
200 , node = new nodes.Text(tok.val);
201 node.line = this.line();
202 return node;
203 },
204
205 /**
206 * code
207 */
208
209 parseCode: function(){
210 var tok = this.expect('code')
211 , node = new nodes.Code(tok.val, tok.buffer, tok.escape);
212 node.line = this.line();
213 if ('indent' == this.peek().type) {
214 node.block = this.parseBlock();
215 }
216 return node;
217 },
218
219 /**
220 * comment
221 */
222
223 parseComment: function(){
224 var tok = this.expect('comment')
225 , node;
226
227 if ('indent' == this.peek().type) {
228 node = new nodes.BlockComment(tok.val, this.parseBlock(), tok.buffer);
229 } else {
230 node = new nodes.Comment(tok.val, tok.buffer);
231 }
232
233 node.line = this.line();
234 return node;
235 },
236
237 /**
238 * doctype
239 */
240
241 parseDoctype: function(){
242 var tok = this.expect('doctype')
243 , node = new nodes.Doctype(tok.val);
244 node.line = this.line();
245 return node;
246 },
247
248 /**
249 * filter attrs? text-block
250 */
251
252 parseFilter: function(){
253 var block
254 , tok = this.expect('filter')
255 , attrs = this.accept('attrs');
256
257 this.lexer.pipeless = true;
258 block = this.parseTextBlock();
259 this.lexer.pipeless = false;
260
261 var node = new nodes.Filter(tok.val, block, attrs && attrs.attrs);
262 node.line = this.line();
263 return node;
264 },
265
266 /**
267 * tag ':' attrs? block
268 */
269
270 parseASTFilter: function(){
271 var block
272 , tok = this.expect('tag')
273 , attrs = this.accept('attrs');
274
275 this.expect(':');
276 block = this.parseBlock();
277
278 var node = new nodes.Filter(tok.val, block, attrs && attrs.attrs);
279 node.line = this.line();
280 return node;
281 },
282
283 /**
284 * each block
285 */
286
287 parseEach: function(){
288 var tok = this.expect('each')
289 , node = new nodes.Each(tok.code, tok.val, tok.key, this.parseBlock());
290 node.line = this.line();
291 return node;
292 },
293
294 /**
295 * include
296 */
297
298 parseInclude: function(){
299 var path = require('path')
300 , fs = require('fs')
301 , dirname = path.dirname
302 , join = path.join;
303
304 if (!this.filename)
305 throw new Error('the "filename" option is required to use includes');
306
307 var path = name = this.expect('include').val.trim()
308 , dir = dirname(this.filename)
309 , path = join(dir, path + '.jade');
310
311 var str = fs.readFileSync(path, 'utf8')
312 , parser = new Parser(str, path)
313 , ast = parser.parse();
314
315 return ast;
316 },
317
318 /**
319 * mixin block
320 */
321
322 parseMixin: function(){
323 var tok = this.expect('mixin')
324 , name = tok.val
325 , args = tok.args;
326 var block = 'indent' == this.peek().type
327 ? this.parseBlock()
328 : null;
329 return new nodes.Mixin(name, args, block);
330 },
331
332 /**
333 * indent (text | newline)* outdent
334 */
335
336 parseTextBlock: function(){
337 var text = new nodes.Text;
338 text.line = this.line();
339 var spaces = this.expect('indent').val;
340 if (null == this._spaces) this._spaces = spaces;
341 var indent = Array(spaces - this._spaces + 1).join(' ');
342 while ('outdent' != this.peek().type) {
343 switch (this.peek().type) {
344 case 'newline':
345 text.push('\\n');
346 this.advance();
347 break;
348 case 'indent':
349 text.push('\\n');
350 this.parseTextBlock().nodes.forEach(function(node){
351 text.push(node);
352 });
353 text.push('\\n');
354 break;
355 default:
356 text.push(indent + this.advance().val);
357 }
358 }
359
360 if (spaces == this._spaces) this._spaces = null;
361 this.expect('outdent');
362 return text;
363 },
364
365 /**
366 * indent expr* outdent
367 */
368
369 parseBlock: function(){
370 var block = new nodes.Block;
371 block.line = this.line();
372 this.expect('indent');
373 while ('outdent' != this.peek().type) {
374 if ('newline' == this.peek().type) {
375 this.advance();
376 } else {
377 block.push(this.parseExpr());
378 }
379 }
380 this.expect('outdent');
381 return block;
382 },
383
384 /**
385 * tag (attrs | class | id)* (text | code | ':')? newline* block?
386 */
387
388 parseTag: function(){
389 // ast-filter look-ahead
390 var i = 2;
391 if ('attrs' == this.lookahead(i).type) ++i;
392 if (':' == this.lookahead(i).type) {
393 if ('indent' == this.lookahead(++i).type) {
394 return this.parseASTFilter();
395 }
396 }
397
398 var name = this.advance().val
399 , tag = new nodes.Tag(name);
400
401 tag.line = this.line();
402
403 // (attrs | class | id)*
404 out:
405 while (true) {
406 switch (this.peek().type) {
407 case 'id':
408 case 'class':
409 var tok = this.advance();
410 tag.setAttribute(tok.type, "'" + tok.val + "'");
411 continue;
412 case 'attrs':
413 var obj = this.advance().attrs
414 , names = Object.keys(obj);
415 for (var i = 0, len = names.length; i < len; ++i) {
416 var name = names[i]
417 , val = obj[name];
418 tag.setAttribute(name, val);
419 }
420 continue;
421 default:
422 break out;
423 }
424 }
425
426 // check immediate '.'
427 if ('.' == this.peek().val) {
428 tag.textOnly = true;
429 this.advance();
430 }
431
432 // (text | code | ':')?
433 switch (this.peek().type) {
434 case 'text':
435 tag.text = this.parseText();
436 break;
437 case 'code':
438 tag.code = this.parseCode();
439 break;
440 case ':':
441 this.advance();
442 tag.block = new nodes.Block;
443 tag.block.push(this.parseTag());
444 break;
445 }
446
447 // newline*
448 while ('newline' == this.peek().type) this.advance();
449
450 tag.textOnly = tag.textOnly || ~textOnly.indexOf(tag.name);
451
452 // script special-case
453 if ('script' == tag.name) {
454 var type = tag.getAttribute('type');
455 if (type && 'text/javascript' != type.replace(/^['"]|['"]$/g, '')) {
456 tag.textOnly = false;
457 }
458 }
459
460 // block?
461 if ('indent' == this.peek().type) {
462 if (tag.textOnly) {
463 this.lexer.pipeless = true;
464 tag.block = this.parseTextBlock();
465 this.lexer.pipeless = false;
466 } else {
467 var block = this.parseBlock();
468 if (tag.block) {
469 for (var i = 0, len = block.nodes.length; i < len; ++i) {
470 tag.block.push(block.nodes[i]);
471 }
472 } else {
473 tag.block = block;
474 }
475 }
476 }
477
478 return tag;
479 }
480};