1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | var Lexer = require('./lexer')
|
13 | , nodes = require('./nodes');
|
14 |
|
15 |
|
16 |
|
17 |
|
18 |
|
19 |
|
20 |
|
21 |
|
22 |
|
23 |
|
24 | var 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 |
|
32 |
|
33 |
|
34 | var textOnly = exports.textOnly = ['code', 'script', 'textarea', 'style', 'title'];
|
35 |
|
36 |
|
37 |
|
38 |
|
39 |
|
40 | Parser.prototype = {
|
41 |
|
42 | |
43 |
|
44 |
|
45 |
|
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 |
|
58 |
|
59 |
|
60 |
|
61 |
|
62 |
|
63 | advance: function(){
|
64 | return this.lexer.advance();
|
65 | },
|
66 |
|
67 | |
68 |
|
69 |
|
70 |
|
71 |
|
72 |
|
73 |
|
74 | peek: function() {
|
75 | return this.lookahead(1);
|
76 | },
|
77 |
|
78 | |
79 |
|
80 |
|
81 |
|
82 |
|
83 |
|
84 |
|
85 | line: function() {
|
86 | return this.lexer.lineno;
|
87 | },
|
88 |
|
89 | |
90 |
|
91 |
|
92 |
|
93 |
|
94 |
|
95 |
|
96 |
|
97 | lookahead: function(n){
|
98 | return this.lexer.lookahead(n);
|
99 | },
|
100 |
|
101 | |
102 |
|
103 |
|
104 |
|
105 |
|
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 |
|
123 |
|
124 |
|
125 |
|
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 |
|
138 |
|
139 |
|
140 |
|
141 |
|
142 |
|
143 | accept: function(type){
|
144 | if (this.peek().type === type) {
|
145 | return this.advance();
|
146 | }
|
147 | },
|
148 |
|
149 | |
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 |
|
157 |
|
158 |
|
159 |
|
160 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
386 |
|
387 |
|
388 | parseTag: function(){
|
389 |
|
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 |
|
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 |
|
427 | if ('.' == this.peek().val) {
|
428 | tag.textOnly = true;
|
429 | this.advance();
|
430 | }
|
431 |
|
432 |
|
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 |
|
448 | while ('newline' == this.peek().type) this.advance();
|
449 |
|
450 | tag.textOnly = tag.textOnly || ~textOnly.indexOf(tag.name);
|
451 |
|
452 |
|
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 |
|
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 | };
|