1 |
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 | exports.version = '0.0.2';
|
13 |
|
14 |
|
15 |
|
16 |
|
17 |
|
18 | var sys = require('sys'),
|
19 | fs = require('fs');
|
20 |
|
21 |
|
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
27 | var cache = exports.cache = {};
|
28 |
|
29 |
|
30 |
|
31 |
|
32 |
|
33 |
|
34 |
|
35 | var selfClosing = exports.selfClosing = [
|
36 | 'meta',
|
37 | 'img',
|
38 | 'link',
|
39 | 'br',
|
40 | 'hr',
|
41 | 'input',
|
42 | 'area',
|
43 | 'base'
|
44 | ];
|
45 |
|
46 |
|
47 |
|
48 |
|
49 |
|
50 |
|
51 |
|
52 | var doctypes = exports.doctypes = {
|
53 | '5': '<!DOCTYPE html>',
|
54 | 'xml': '<?xml version="1.0" encoding="utf-8" ?>',
|
55 | 'default': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
|
56 | 'transitional': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">',
|
57 | 'strict': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">',
|
58 | 'frameset': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">',
|
59 | '1.1': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">',
|
60 | 'basic': '<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML Basic 1.1//EN" "http://www.w3.org/TR/xhtml-basic/xhtml-basic11.dtd">',
|
61 | 'mobile': '<!DOCTYPE html PUBLIC "-//WAPFORUM//DTD XHTML Mobile 1.2//EN" "http://www.openmobilealliance.org/tech/DTD/xhtml-mobile12.dtd">'
|
62 | };
|
63 |
|
64 |
|
65 |
|
66 |
|
67 |
|
68 |
|
69 |
|
70 | var filters = exports.filters = {
|
71 |
|
72 | |
73 |
|
74 |
|
75 |
|
76 | cdata: function(str){
|
77 | return '<![CDATA[\\n' + str + '\\n]]>';
|
78 | },
|
79 |
|
80 | |
81 |
|
82 |
|
83 |
|
84 | javascript: function(str){
|
85 | return '<script type="text/javascript">\\n//<![CDATA[\\n' + str + '\\n//]]></script>';
|
86 | },
|
87 |
|
88 | |
89 |
|
90 |
|
91 |
|
92 | sass: function(str){
|
93 | str = str.replace(/\\n/g, '\n');
|
94 | var sass = require('sass').render(str).replace(/\n/g, '\\n');
|
95 | return '<style>' + sass + '</style>';
|
96 | },
|
97 |
|
98 | |
99 |
|
100 |
|
101 |
|
102 | markdown: function(str){
|
103 | str = str.replace(/\\n/g, '\n');
|
104 | return require('markdown').parse(str).replace(/\n/g, '\\n');
|
105 | }
|
106 | };
|
107 |
|
108 |
|
109 |
|
110 |
|
111 |
|
112 |
|
113 |
|
114 |
|
115 |
|
116 | function Parser(str, filename){
|
117 | this.input = str.replace(/\r\n|\r/g, '\n');
|
118 | this.filename = filename;
|
119 | this.deferredTokens = [];
|
120 | this.lastIndents = 0;
|
121 | this.lineno = 1;
|
122 | }
|
123 |
|
124 |
|
125 |
|
126 |
|
127 |
|
128 | Parser.prototype = {
|
129 |
|
130 | |
131 |
|
132 |
|
133 |
|
134 |
|
135 |
|
136 | debug: function(){
|
137 | var tok, width = 8;
|
138 | while ((tok = this.advance).type !== 'eos') {
|
139 | var type = tok.type,
|
140 | pad = width - type.length;
|
141 | while (pad--) type += ' ';
|
142 | sys.puts(tok.line
|
143 | + ' : \x1B[1m' + type + '\x1B[0m'
|
144 | + ' ' + sys.inspect(tok.val)
|
145 | + (tok.attrs ? ' ' + sys.inspect(tok.attrs) : ''));
|
146 | }
|
147 | },
|
148 |
|
149 | |
150 |
|
151 |
|
152 |
|
153 |
|
154 |
|
155 |
|
156 | get advance(){
|
157 | var self = this,
|
158 | captures;
|
159 |
|
160 | if (this.stash) {
|
161 | var tok = this.stash;
|
162 | delete this.stash;
|
163 | return tok;
|
164 | }
|
165 |
|
166 | if (this.deferredTokens.length) {
|
167 | return this.deferredTokens.shift();
|
168 | }
|
169 |
|
170 | |
171 |
|
172 |
|
173 |
|
174 | function token(type){
|
175 | self.input = self.input.substr(captures[0].length);
|
176 | return {
|
177 | type: type,
|
178 | line: self.lineno,
|
179 | val: captures[1]
|
180 | };
|
181 | }
|
182 |
|
183 |
|
184 | if (!this.input.length) {
|
185 | if (this.lastIndents-- > 0) {
|
186 | return { type: 'outdent', line: this.lineno };
|
187 | } else {
|
188 | return { type: 'eos', line: this.lineno };
|
189 | }
|
190 | }
|
191 |
|
192 |
|
193 | if (captures = /^(\w[:\w]*)/.exec(this.input)) {
|
194 | return token('tag');
|
195 | }
|
196 |
|
197 |
|
198 | if (captures = /^:(\w+)/.exec(this.input)) {
|
199 | return token('filter');
|
200 | }
|
201 |
|
202 |
|
203 | if (captures = /^(!?=|-)([^\n]+)/.exec(this.input)) {
|
204 | var flags = captures[1];
|
205 | captures[1] = captures[2];
|
206 | var tok = token('code');
|
207 | tok.escape = flags[0] === '=';
|
208 | tok.buffer = flags[0] === '=' || flags[1] === '=';
|
209 | return tok;
|
210 | }
|
211 |
|
212 |
|
213 | if (captures = /^!!! *(\w+)?/.exec(this.input)) {
|
214 | return token('doctype');
|
215 | }
|
216 |
|
217 |
|
218 | if (captures = /^#([\w-]+)/.exec(this.input)) {
|
219 | return token('id');
|
220 | }
|
221 |
|
222 |
|
223 | if (captures = /^\.([\w-]+)/.exec(this.input)) {
|
224 | return token('class');
|
225 | }
|
226 |
|
227 |
|
228 | if (captures = /^\( *(.+) *\)/.exec(this.input)) {
|
229 | var tok = token('attrs'),
|
230 | attrs = tok.val.split(/ *, */);
|
231 | tok.attrs = {};
|
232 | for (var i = 0, len = attrs.length; i < len; ++i) {
|
233 | var pair = attrs[i];
|
234 |
|
235 |
|
236 | var colon = pair.indexOf(':'),
|
237 | equal = pair.indexOf('=');
|
238 |
|
239 |
|
240 | if (colon < 0 && equal < 0) {
|
241 | var key = pair,
|
242 | val = true;
|
243 | } else {
|
244 |
|
245 | var split = equal >= 0
|
246 | ? equal
|
247 | : colon;
|
248 | if (colon >= 0 && colon < equal) split = colon;
|
249 | var key = pair.substr(0, split),
|
250 | val = pair.substr(++split, pair.length);
|
251 | }
|
252 | tok.attrs[key.trim()] = val;
|
253 | }
|
254 | return tok;
|
255 | }
|
256 |
|
257 |
|
258 | if (captures = /^\n( *)/.exec(this.input)) {
|
259 | ++this.lineno;
|
260 | var tok = token('indent'),
|
261 | indents = tok.val.length / 2;
|
262 | if (this.input[0] === '\n') {
|
263 | tok.type = 'newline';
|
264 | return tok;
|
265 | } else if (indents % 1 !== 0) {
|
266 | throw new Error('Invalid indentation, got '
|
267 | + tok.val.length + ' space'
|
268 | + (tok.val.length > 1 ? 's' : '')
|
269 | + ', must be a multiple of two.');
|
270 | } else if (indents === this.lastIndents) {
|
271 | tok.type = 'newline';
|
272 | } else if (indents > this.lastIndents + 1) {
|
273 | throw new Error('Invalid indentation, got '
|
274 | + indents + ' expected '
|
275 | + (this.lastIndents + 1) + '.');
|
276 | } else if (indents < this.lastIndents) {
|
277 | var n = this.lastIndents - indents;
|
278 | tok.type = 'outdent';
|
279 | while (--n) {
|
280 | this.deferredTokens.push({
|
281 | type: 'outdent',
|
282 | line: this.lineno
|
283 | });
|
284 | }
|
285 | }
|
286 | this.lastIndents = indents;
|
287 | return tok;
|
288 | }
|
289 |
|
290 |
|
291 | if (captures = /^(?:\| ?)?([^\n]+)/.exec(this.input)) {
|
292 | return token('text');
|
293 | }
|
294 | },
|
295 |
|
296 | |
297 |
|
298 |
|
299 |
|
300 |
|
301 |
|
302 |
|
303 | get peek() {
|
304 | return this.stash = this.advance;
|
305 | },
|
306 |
|
307 | |
308 |
|
309 |
|
310 |
|
311 |
|
312 |
|
313 |
|
314 | get _() {
|
315 | return '_.lineno = ' + this.lineno + ';';
|
316 | },
|
317 |
|
318 | |
319 |
|
320 |
|
321 |
|
322 |
|
323 |
|
324 |
|
325 | parse: function(){
|
326 | var buf = ['var buf = [];'];
|
327 | while (this.peek.type !== 'eos') {
|
328 | buf.push(this.parseExpr());
|
329 | }
|
330 | buf.push("return buf.join('');");
|
331 | return buf.join('\n');
|
332 | },
|
333 |
|
334 | |
335 |
|
336 |
|
337 |
|
338 |
|
339 |
|
340 |
|
341 | expect: function(type){
|
342 | if (this.peek.type === type) {
|
343 | return this.advance;
|
344 | } else {
|
345 | throw new Error('expected "' + type + '", but got "' + this.peek.type + '"');
|
346 | }
|
347 | },
|
348 |
|
349 | |
350 |
|
351 |
|
352 |
|
353 |
|
354 |
|
355 |
|
356 |
|
357 |
|
358 |
|
359 |
|
360 | parseExpr: function(){
|
361 | switch (this.peek.type) {
|
362 | case 'tag':
|
363 | return this.parseTag();
|
364 | case 'doctype':
|
365 | return this.parseDoctype();
|
366 | case 'filter':
|
367 | return this.parseFilter();
|
368 | case 'text':
|
369 | return "buf.push('" + interpolate(this.advance.val.replace(/'/g, "\\'")) + " ');";
|
370 | case 'id':
|
371 | case 'class':
|
372 | var tok = this.advance;
|
373 | this.deferredTokens.push({
|
374 | val: 'div',
|
375 | type: 'tag',
|
376 | line: this.lineno
|
377 | });
|
378 | this.deferredTokens.push(tok);
|
379 | return this.parseExpr();
|
380 | case 'code':
|
381 | var tok = this.advance,
|
382 | val = tok.val;
|
383 | var buf = tok.buffer
|
384 | ? 'buf.push(' + (tok.escape
|
385 | ? 'escape(' + val + ')'
|
386 | : val) + ')'
|
387 | : val;
|
388 | return this.peek.type === 'indent'
|
389 | ? buf + '\n(function(){' + this.parseBlock() + '})();'
|
390 | : buf + ';';
|
391 | case 'newline':
|
392 | this.advance;
|
393 | return this._ + this.parseExpr();
|
394 | }
|
395 | },
|
396 |
|
397 | |
398 |
|
399 |
|
400 |
|
401 | parseDoctype: function(){
|
402 | var name = this.expect('doctype').val;
|
403 | if (name === '5') this.mode = 'html 5';
|
404 | return "buf.push('" + doctypes[name || 'default'] + "');";
|
405 | },
|
406 |
|
407 | |
408 |
|
409 |
|
410 |
|
411 | parseFilter: function(){
|
412 | var name = this.expect('filter').val,
|
413 | filter = filters[name];
|
414 | if (filter) {
|
415 | return "buf.push('" + filter(interpolate(this.parseTextBlock())) + "');";
|
416 | } else {
|
417 | throw new Error('unknown filter ":' + name + '"');
|
418 | }
|
419 | },
|
420 |
|
421 | |
422 |
|
423 |
|
424 |
|
425 | parseTextBlock: function(){
|
426 | var buf = [];
|
427 | this.expect('indent');
|
428 | while (this.peek.type === 'text' || this.peek.type === 'newline') {
|
429 | if (this.peek.type === 'newline') {
|
430 | this.advance;
|
431 | buf.push('\\n');
|
432 | } else {
|
433 | buf.push(this.advance.val);
|
434 | }
|
435 | }
|
436 | this.expect('outdent');
|
437 | return buf.join('');
|
438 | },
|
439 |
|
440 | |
441 |
|
442 |
|
443 |
|
444 | parseBlock: function(){
|
445 | var buf = [];
|
446 | buf.push(this._); this.expect('indent');
|
447 | while (this.peek.type !== 'outdent') {
|
448 | buf.push(this.parseExpr());
|
449 | }
|
450 | this.expect('outdent');
|
451 | return buf.join('\n');
|
452 | },
|
453 |
|
454 | |
455 |
|
456 |
|
457 |
|
458 | parseTag: function(){
|
459 | var name = this.advance.val,
|
460 | html5 = this.mode === 'html 5',
|
461 | hasAttrs = false,
|
462 | attrBuf = '',
|
463 | codeClass = '',
|
464 | classes = [],
|
465 | attrs = {},
|
466 | buf = [];
|
467 |
|
468 |
|
469 | out:
|
470 | while (1) {
|
471 | switch (this.peek.type) {
|
472 | case 'id':
|
473 | hasAttrs = true;
|
474 | attrs.id = '"' + this.advance.val + '"';
|
475 | continue;
|
476 | case 'class':
|
477 | hasAttrs = true;
|
478 | classes.push(this.advance.val);
|
479 | continue;
|
480 | case 'attrs':
|
481 | hasAttrs = true;
|
482 | var obj = this.advance.attrs,
|
483 | keys = Object.keys(obj);
|
484 | for (var i = 0, len = keys.length; i < len; ++i) {
|
485 | var key = keys[i],
|
486 | val = obj[key];
|
487 | if (key === 'class') {
|
488 | codeClass = val;
|
489 | } else {
|
490 | attrs[key] = val === undefined
|
491 | ? true
|
492 | : val;
|
493 | attrs.html5 = html5;
|
494 | }
|
495 | }
|
496 | continue;
|
497 | default:
|
498 | break out;
|
499 | }
|
500 | }
|
501 |
|
502 |
|
503 | switch (this.peek.type) {
|
504 | case 'text':
|
505 | buf.push("buf.push('" + interpolate(this.advance.val.trim().replace(/'/g, "\\'")) + "');");
|
506 | break;
|
507 | case 'code':
|
508 | var tok = this.advance;
|
509 | if (tok.buffer) {
|
510 | buf.push('buf.push(' + (tok.escape
|
511 | ? 'escape(' + tok.val + ')'
|
512 | : tok.val) + ');');
|
513 | } else {
|
514 | buf.push(tok.val + ';');
|
515 | }
|
516 | break;
|
517 | case 'indent':
|
518 | buf.push(this.parseBlock());
|
519 | break;
|
520 | }
|
521 |
|
522 |
|
523 | if (hasAttrs) {
|
524 |
|
525 | if (classes.length) {
|
526 | attrs['class'] = '"' + classes.join(' ') + '"';
|
527 | }
|
528 | if (codeClass) {
|
529 | if (attrs['class']) {
|
530 | attrs['class'] += ' + " " + (' + codeClass + ')';
|
531 | } else {
|
532 | attrs['class'] = codeClass;
|
533 | }
|
534 | }
|
535 |
|
536 |
|
537 | attrBuf += "' + attrs({ ";
|
538 | var keys = Object.keys(attrs);
|
539 | for (var i = 0, len = keys.length; i < len; ++i) {
|
540 | var key = keys[i],
|
541 | val = attrs[key];
|
542 | attrBuf += "'" + key + "': " + val + (i === len - 1 ? '' : ', ');
|
543 | }
|
544 | attrBuf += " }) + '";
|
545 | } else {
|
546 | attrBuf = "' + '";
|
547 | }
|
548 |
|
549 |
|
550 | if (selfClosing.indexOf(name) >= 0) {
|
551 | return [
|
552 | "buf.push('<" + name + attrBuf + (html5 ? '' : ' /' ) + ">');",
|
553 | buf.join('\n')
|
554 | ].join('\n');
|
555 | } else {
|
556 | return [
|
557 | "buf.push('<" + name + attrBuf + ">');",
|
558 | buf.join('\n'),
|
559 | "buf.push('</" + name + ">');"
|
560 | ].join('\n');
|
561 | }
|
562 | }
|
563 | };
|
564 |
|
565 |
|
566 |
|
567 |
|
568 |
|
569 |
|
570 |
|
571 |
|
572 |
|
573 | function attrs(obj){
|
574 | var buf = [],
|
575 | html5 = obj.html5;
|
576 | delete obj.html5;
|
577 | var keys = Object.keys(obj);
|
578 | len = keys.length;
|
579 | if (len) {
|
580 | buf.push('');
|
581 | for (var i = 0; i < len; ++i) {
|
582 | var key = keys[i],
|
583 | val = obj[key];
|
584 | if (typeof val === 'boolean' || val === '' || val == null) {
|
585 | if (val) {
|
586 | html5
|
587 | ? buf.push(key)
|
588 | : buf.push(key + '="' + key + '"');
|
589 | }
|
590 | } else {
|
591 | buf.push(key + '="' + escape(val) + '"');
|
592 | }
|
593 | }
|
594 | }
|
595 | return buf.join(' ');
|
596 | }
|
597 |
|
598 |
|
599 |
|
600 |
|
601 |
|
602 |
|
603 |
|
604 |
|
605 |
|
606 | function escape(html){
|
607 | return String(html)
|
608 | .replace(/&(?!\w+;)/g, '&')
|
609 | .replace(/</g, '<')
|
610 | .replace(/>/g, '>')
|
611 | .replace(/"/g, '"');
|
612 | }
|
613 |
|
614 |
|
615 |
|
616 |
|
617 |
|
618 |
|
619 |
|
620 |
|
621 |
|
622 | function interpolate(str){
|
623 | return str.replace(/(\\)?#{(.*?)}/g, function(str, escape, code){
|
624 | return escape
|
625 | ? str
|
626 | : "' + (" + code + ") + '";
|
627 | });
|
628 | }
|
629 |
|
630 |
|
631 |
|
632 |
|
633 |
|
634 |
|
635 |
|
636 |
|
637 |
|
638 |
|
639 |
|
640 |
|
641 |
|
642 |
|
643 |
|
644 |
|
645 |
|
646 | exports.render = function(str, options){
|
647 | var js,
|
648 | options = options || {},
|
649 | filename = options.filename;
|
650 |
|
651 |
|
652 | function parse(){
|
653 | try {
|
654 | var parser = new Parser(str, filename);
|
655 | if (options.debug) {
|
656 | parser.debug();
|
657 | parser = new Parser(str, filename);
|
658 | }
|
659 | var js = parser.parse()
|
660 | if (options.debug) sys.puts('\nfunction:', js.replace(/^/gm, ' '));
|
661 | return js;
|
662 | } catch (err) {
|
663 | rethrow(err, parser.lineno);
|
664 | }
|
665 | }
|
666 |
|
667 |
|
668 | function rethrow(err, lineno){
|
669 | var start = lineno - 3 > 0
|
670 | ? lineno - 3
|
671 | : 0;
|
672 |
|
673 | var context = str.split('\n').slice(start, lineno).map(function(line, i){
|
674 | return ' ' + (i + start + 1) + '. ' + sys.inspect(line);
|
675 | }).join('\n');
|
676 |
|
677 |
|
678 | err.path = filename;
|
679 | err.message = (filename || 'Jade') + ':' + lineno
|
680 | + '\n' + context + '\n\n' + err.message;
|
681 | throw err;
|
682 | }
|
683 |
|
684 |
|
685 | if (options.cache) {
|
686 | if (filename) {
|
687 | if (cache[filename]) {
|
688 | js = cache[filename];
|
689 | } else {
|
690 | js = cache[filename] = parse();
|
691 | }
|
692 | } else {
|
693 | throw new Error('filename is required when using the cache option');
|
694 | }
|
695 | } else {
|
696 | js = parse();
|
697 | }
|
698 |
|
699 |
|
700 | try {
|
701 | var fn = Function('locals, attrs, escape, _', 'with (locals) {' + js + '}');
|
702 | } catch (err) {
|
703 |
|
704 | process.compile(js, filename || 'Jade');
|
705 | return;
|
706 | }
|
707 |
|
708 | try {
|
709 | var _ = { lineno: 1 };
|
710 | return fn.call(options.scope || options.context,
|
711 | options.locals || {},
|
712 | attrs,
|
713 | escape,
|
714 | _);
|
715 | } catch (err) {
|
716 | rethrow(err, _.lineno);
|
717 | }
|
718 | };
|
719 |
|
720 |
|
721 |
|
722 |
|
723 |
|
724 |
|
725 |
|
726 |
|
727 |
|
728 |
|
729 | exports.renderFile = function(path, options, fn){
|
730 | if (typeof options === 'function') {
|
731 | fn = options;
|
732 | options = {};
|
733 | }
|
734 | options.filename = path;
|
735 | fs.readFile(path, 'utf8', function(err, str){
|
736 | if (err) {
|
737 | fn(err);
|
738 | } else {
|
739 | try {
|
740 | fn(null, exports.render(str, options));
|
741 | } catch (err) {
|
742 | fn(err);
|
743 | }
|
744 | }
|
745 | });
|
746 | }; |
\ | No newline at end of file |