UNPKG

22.3 kBJavaScriptView Raw
1'use strict';
2
3var utils = require('./utils');
4var characterParser = require('character-parser');
5
6
7/**
8 * Initialize `Lexer` with the given `str`.
9 *
10 * @param {String} str
11 * @param {String} filename
12 * @api private
13 */
14
15var Lexer = module.exports = function Lexer(str, filename) {
16 this.input = str.replace(/\r\n|\r/g, '\n');
17 this.filename = filename;
18 this.deferredTokens = [];
19 this.lastIndents = 0;
20 this.lineno = 1;
21 this.stash = [];
22 this.indentStack = [];
23 this.indentRe = null;
24 this.pipeless = false;
25};
26
27
28function assertExpression(exp) {
29 //this verifies that a JavaScript expression is valid
30 Function('', 'return (' + exp + ')');
31}
32function assertNestingCorrect(exp) {
33 //this verifies that code is properly nested, but allows
34 //invalid JavaScript such as the contents of `attributes`
35 var res = characterParser(exp)
36 if (res.isNesting()) {
37 throw new Error('Nesting must match on expression `' + exp + '`')
38 }
39}
40
41/**
42 * Lexer prototype.
43 */
44
45Lexer.prototype = {
46
47 /**
48 * Construct a token with the given `type` and `val`.
49 *
50 * @param {String} type
51 * @param {String} val
52 * @return {Object}
53 * @api private
54 */
55
56 tok: function(type, val){
57 return {
58 type: type
59 , line: this.lineno
60 , val: val
61 }
62 },
63
64 /**
65 * Consume the given `len` of input.
66 *
67 * @param {Number} len
68 * @api private
69 */
70
71 consume: function(len){
72 this.input = this.input.substr(len);
73 },
74
75 /**
76 * Scan for `type` with the given `regexp`.
77 *
78 * @param {String} type
79 * @param {RegExp} regexp
80 * @return {Object}
81 * @api private
82 */
83
84 scan: function(regexp, type){
85 var captures;
86 if (captures = regexp.exec(this.input)) {
87 this.consume(captures[0].length);
88 return this.tok(type, captures[1]);
89 }
90 },
91
92 /**
93 * Defer the given `tok`.
94 *
95 * @param {Object} tok
96 * @api private
97 */
98
99 defer: function(tok){
100 this.deferredTokens.push(tok);
101 },
102
103 /**
104 * Lookahead `n` tokens.
105 *
106 * @param {Number} n
107 * @return {Object}
108 * @api private
109 */
110
111 lookahead: function(n){
112 var fetch = n - this.stash.length;
113 while (fetch-- > 0) this.stash.push(this.next());
114 return this.stash[--n];
115 },
116
117 /**
118 * Return the indexOf `(` or `{` or `[` / `)` or `}` or `]` delimiters.
119 *
120 * @return {Number}
121 * @api private
122 */
123
124 bracketExpression: function(skip){
125 skip = skip || 0;
126 var start = this.input[skip];
127 if (start != '(' && start != '{' && start != '[') throw new Error('unrecognized start character');
128 var end = ({'(': ')', '{': '}', '[': ']'})[start];
129 var range = characterParser.parseMax(this.input, {start: skip + 1});
130 if (this.input[range.end] !== end) throw new Error('start character ' + start + ' does not match end character ' + this.input[range.end]);
131 return range;
132 },
133
134 /**
135 * Stashed token.
136 */
137
138 stashed: function() {
139 return this.stash.length
140 && this.stash.shift();
141 },
142
143 /**
144 * Deferred token.
145 */
146
147 deferred: function() {
148 return this.deferredTokens.length
149 && this.deferredTokens.shift();
150 },
151
152 /**
153 * end-of-source.
154 */
155
156 eos: function() {
157 if (this.input.length) return;
158 if (this.indentStack.length) {
159 this.indentStack.shift();
160 return this.tok('outdent');
161 } else {
162 return this.tok('eos');
163 }
164 },
165
166 /**
167 * Blank line.
168 */
169
170 blank: function() {
171 var captures;
172 if (captures = /^\n *\n/.exec(this.input)) {
173 this.consume(captures[0].length - 1);
174 ++this.lineno;
175 if (this.pipeless) return this.tok('text', '');
176 return this.next();
177 }
178 },
179
180 /**
181 * Comment.
182 */
183
184 comment: function() {
185 var captures;
186 if (captures = /^\/\/(-)?([^\n]*)/.exec(this.input)) {
187 this.consume(captures[0].length);
188 var tok = this.tok('comment', captures[2]);
189 tok.buffer = '-' != captures[1];
190 this.pipeless = true;
191 return tok;
192 }
193 },
194
195 /**
196 * Interpolated tag.
197 */
198
199 interpolation: function() {
200 if (/^#\{/.test(this.input)) {
201 var match = this.bracketExpression(1);
202
203 this.consume(match.end + 1);
204 return this.tok('interpolation', match.src);
205 }
206 },
207
208 /**
209 * Tag.
210 */
211
212 tag: function() {
213 var captures;
214 if (captures = /^(\w[-:\w]*)(\/?)/.exec(this.input)) {
215 this.consume(captures[0].length);
216 var tok, name = captures[1];
217 if (':' == name[name.length - 1]) {
218 name = name.slice(0, -1);
219 tok = this.tok('tag', name);
220 this.defer(this.tok(':'));
221 if (this.input[0] !== ' ') {
222 console.warn('Warning: space required after `:` on line ' + this.lineno +
223 ' of jade file "' + this.filename + '"');
224 }
225 while (' ' == this.input[0]) this.input = this.input.substr(1);
226 } else {
227 tok = this.tok('tag', name);
228 }
229 tok.selfClosing = !!captures[2];
230 return tok;
231 }
232 },
233
234 /**
235 * Filter.
236 */
237
238 filter: function() {
239 var tok = this.scan(/^:([\w\-]+)/, 'filter');
240 if (tok) {
241 this.pipeless = true;
242 return tok;
243 }
244 },
245
246 /**
247 * Doctype.
248 */
249
250 doctype: function() {
251 if (this.scan(/^!!! *([^\n]+)?/, 'doctype')) {
252 throw new Error('`!!!` is deprecated, you must now use `doctype`');
253 }
254 var node = this.scan(/^(?:doctype) *([^\n]+)?/, 'doctype');
255 if (node && node.val && node.val.trim() === '5') {
256 throw new Error('`doctype 5` is deprecated, you must now use `doctype html`');
257 }
258 return node;
259 },
260
261 /**
262 * Id.
263 */
264
265 id: function() {
266 return this.scan(/^#([\w-]+)/, 'id');
267 },
268
269 /**
270 * Class.
271 */
272
273 className: function() {
274 return this.scan(/^\.([\w-]+)/, 'class');
275 },
276
277 /**
278 * Text.
279 */
280
281 text: function() {
282 return this.scan(/^(?:\| ?| )([^\n]+)/, 'text') ||
283 this.scan(/^\|?( )/, 'text') ||
284 this.scan(/^(<[^\n]*)/, 'text');
285 },
286
287 textFail: function () {
288 var tok;
289 if (tok = this.scan(/^([^\.\n][^\n]+)/, 'text')) {
290 console.warn('Warning: missing space before text for line ' + this.lineno +
291 ' of jade file "' + this.filename + '"');
292 return tok;
293 }
294 },
295
296 /**
297 * Dot.
298 */
299
300 dot: function() {
301 var match;
302 if (match = this.scan(/^\./, 'dot')) {
303 this.pipeless = true;
304 return match;
305 }
306 },
307
308 /**
309 * Extends.
310 */
311
312 "extends": function() {
313 return this.scan(/^extends? +([^\n]+)/, 'extends');
314 },
315
316 /**
317 * Block prepend.
318 */
319
320 prepend: function() {
321 var captures;
322 if (captures = /^prepend +([^\n]+)/.exec(this.input)) {
323 this.consume(captures[0].length);
324 var mode = 'prepend'
325 , name = captures[1]
326 , tok = this.tok('block', name);
327 tok.mode = mode;
328 return tok;
329 }
330 },
331
332 /**
333 * Block append.
334 */
335
336 append: function() {
337 var captures;
338 if (captures = /^append +([^\n]+)/.exec(this.input)) {
339 this.consume(captures[0].length);
340 var mode = 'append'
341 , name = captures[1]
342 , tok = this.tok('block', name);
343 tok.mode = mode;
344 return tok;
345 }
346 },
347
348 /**
349 * Block.
350 */
351
352 block: function() {
353 var captures;
354 if (captures = /^block\b *(?:(prepend|append) +)?([^\n]+)/.exec(this.input)) {
355 this.consume(captures[0].length);
356 var mode = captures[1] || 'replace'
357 , name = captures[2]
358 , tok = this.tok('block', name);
359
360 tok.mode = mode;
361 return tok;
362 }
363 },
364
365 /**
366 * Mixin Block.
367 */
368
369 mixinBlock: function() {
370 var captures;
371 if (captures = /^block[ \t]*(\n|$)/.exec(this.input)) {
372 this.consume(captures[0].length - captures[1].length);
373 return this.tok('mixin-block');
374 }
375 },
376
377 /**
378 * Yield.
379 */
380
381 'yield': function() {
382 return this.scan(/^yield */, 'yield');
383 },
384
385 /**
386 * Include.
387 */
388
389 include: function() {
390 return this.scan(/^include +([^\n]+)/, 'include');
391 },
392
393 /**
394 * Include with filter
395 */
396
397 includeFiltered: function() {
398 var captures;
399 if (captures = /^include:([\w\-]+)([\( ])/.exec(this.input)) {
400 this.consume(captures[0].length - 1);
401 var filter = captures[1];
402 var attrs = captures[2] === '(' ? this.attrs() : null;
403 if (!(captures[2] === ' ' || this.input[0] === ' ')) {
404 throw new Error('expected space after include:filter but got ' + utils.stringify(this.input[0]));
405 }
406 captures = /^ *([^\n]+)/.exec(this.input);
407 if (!captures || captures[1].trim() === '') {
408 throw new Error('missing path for include:filter');
409 }
410 this.consume(captures[0].length);
411 var path = captures[1];
412 var tok = this.tok('include', path);
413 tok.filter = filter;
414 tok.attrs = attrs;
415 return tok;
416 }
417 },
418
419 /**
420 * Case.
421 */
422
423 "case": function() {
424 return this.scan(/^case +([^\n]+)/, 'case');
425 },
426
427 /**
428 * When.
429 */
430
431 when: function() {
432 return this.scan(/^when +([^:\n]+)/, 'when');
433 },
434
435 /**
436 * Default.
437 */
438
439 "default": function() {
440 return this.scan(/^default */, 'default');
441 },
442
443 /**
444 * Call mixin.
445 */
446
447 call: function(){
448
449 var tok, captures;
450 if (captures = /^\+(\s*)(([-\w]+)|(#\{))/.exec(this.input)) {
451 // try to consume simple or interpolated call
452 if (captures[3]) {
453 // simple call
454 this.consume(captures[0].length);
455 tok = this.tok('call', captures[3]);
456 } else {
457 // interpolated call
458 var match = this.bracketExpression(2 + captures[1].length);
459 this.consume(match.end + 1);
460 assertExpression(match.src);
461 tok = this.tok('call', '#{'+match.src+'}');
462 }
463
464 // Check for args (not attributes)
465 if (captures = /^ *\(/.exec(this.input)) {
466 var range = this.bracketExpression(captures[0].length - 1);
467 if (!/^\s*[-\w]+ *=/.test(range.src)) { // not attributes
468 this.consume(range.end + 1);
469 tok.args = range.src;
470 }
471 if (tok.args) {
472 assertExpression('[' + tok.args + ']');
473 }
474 }
475
476 return tok;
477 }
478 },
479
480 /**
481 * Mixin.
482 */
483
484 mixin: function(){
485 var captures;
486 if (captures = /^mixin +([-\w]+)(?: *\((.*)\))? */.exec(this.input)) {
487 this.consume(captures[0].length);
488 var tok = this.tok('mixin', captures[1]);
489 tok.args = captures[2];
490 return tok;
491 }
492 },
493
494 /**
495 * Conditional.
496 */
497
498 conditional: function() {
499 var captures;
500 if (captures = /^(if|unless|else if|else)\b([^\n]*)/.exec(this.input)) {
501 this.consume(captures[0].length);
502 var type = captures[1]
503 var js = captures[2];
504 var isIf = false;
505 var isElse = false;
506
507 switch (type) {
508 case 'if':
509 assertExpression(js)
510 js = 'if (' + js + ')';
511 isIf = true;
512 break;
513 case 'unless':
514 assertExpression(js)
515 js = 'if (!(' + js + '))';
516 isIf = true;
517 break;
518 case 'else if':
519 assertExpression(js)
520 js = 'else if (' + js + ')';
521 isIf = true;
522 isElse = true;
523 break;
524 case 'else':
525 if (js && js.trim()) {
526 throw new Error('`else` cannot have a condition, perhaps you meant `else if`');
527 }
528 js = 'else';
529 isElse = true;
530 break;
531 }
532 var tok = this.tok('code', js);
533 tok.isElse = isElse;
534 tok.isIf = isIf;
535 tok.requiresBlock = true;
536 return tok;
537 }
538 },
539
540 /**
541 * While.
542 */
543
544 "while": function() {
545 var captures;
546 if (captures = /^while +([^\n]+)/.exec(this.input)) {
547 this.consume(captures[0].length);
548 assertExpression(captures[1])
549 var tok = this.tok('code', 'while (' + captures[1] + ')');
550 tok.requiresBlock = true;
551 return tok;
552 }
553 },
554
555 /**
556 * Each.
557 */
558
559 each: function() {
560 var captures;
561 if (captures = /^(?:- *)?(?:each|for) +([a-zA-Z_$][\w$]*)(?: *, *([a-zA-Z_$][\w$]*))? * in *([^\n]+)/.exec(this.input)) {
562 this.consume(captures[0].length);
563 var tok = this.tok('each', captures[1]);
564 tok.key = captures[2] || '$index';
565 assertExpression(captures[3])
566 tok.code = captures[3];
567 return tok;
568 }
569 },
570
571 /**
572 * Code.
573 */
574
575 code: function() {
576 var captures;
577 if (captures = /^(!?=|-)[ \t]*([^\n]+)/.exec(this.input)) {
578 this.consume(captures[0].length);
579 var flags = captures[1];
580 captures[1] = captures[2];
581 var tok = this.tok('code', captures[1]);
582 tok.escape = flags.charAt(0) === '=';
583 tok.buffer = flags.charAt(0) === '=' || flags.charAt(1) === '=';
584 if (tok.buffer) assertExpression(captures[1])
585 return tok;
586 }
587 },
588
589
590 /**
591 * Block code.
592 */
593
594 blockCode: function() {
595 var captures;
596 if (captures = /^-\n/.exec(this.input)) {
597 this.consume(captures[0].length - 1);
598 var tok = this.tok('blockCode');
599 this.pipeless = true;
600 return tok;
601 }
602 },
603
604 /**
605 * Attributes.
606 */
607
608 attrs: function() {
609 if ('(' == this.input.charAt(0)) {
610 var index = this.bracketExpression().end
611 , str = this.input.substr(1, index-1)
612 , tok = this.tok('attrs');
613
614 assertNestingCorrect(str);
615
616 var quote = '';
617 var interpolate = function (attr) {
618 return attr.replace(/(\\)?#\{(.+)/g, function(_, escape, expr){
619 if (escape) return _;
620 try {
621 var range = characterParser.parseMax(expr);
622 if (expr[range.end] !== '}') return _.substr(0, 2) + interpolate(_.substr(2));
623 assertExpression(range.src)
624 return quote + " + (" + range.src + ") + " + quote + interpolate(expr.substr(range.end + 1));
625 } catch (ex) {
626 return _.substr(0, 2) + interpolate(_.substr(2));
627 }
628 });
629 }
630
631 this.consume(index + 1);
632 tok.attrs = [];
633
634 var escapedAttr = true
635 var key = '';
636 var val = '';
637 var interpolatable = '';
638 var state = characterParser.defaultState();
639 var loc = 'key';
640 var isEndOfAttribute = function (i) {
641 if (key.trim() === '') return false;
642 if (i === str.length) return true;
643 if (loc === 'key') {
644 if (str[i] === ' ' || str[i] === '\n') {
645 for (var x = i; x < str.length; x++) {
646 if (str[x] != ' ' && str[x] != '\n') {
647 if (str[x] === '=' || str[x] === '!' || str[x] === ',') return false;
648 else return true;
649 }
650 }
651 }
652 return str[i] === ','
653 } else if (loc === 'value' && !state.isNesting()) {
654 try {
655 assertExpression(val);
656 if (str[i] === ' ' || str[i] === '\n') {
657 for (var x = i; x < str.length; x++) {
658 if (str[x] != ' ' && str[x] != '\n') {
659 if (characterParser.isPunctuator(str[x]) && str[x] != '"' && str[x] != "'") return false;
660 else return true;
661 }
662 }
663 }
664 return str[i] === ',';
665 } catch (ex) {
666 return false;
667 }
668 }
669 }
670
671 this.lineno += str.split("\n").length - 1;
672
673 for (var i = 0; i <= str.length; i++) {
674 if (isEndOfAttribute(i)) {
675 val = val.trim();
676 if (val) assertExpression(val)
677 key = key.trim();
678 key = key.replace(/^['"]|['"]$/g, '');
679 tok.attrs.push({
680 name: key,
681 val: '' == val ? true : val,
682 escaped: escapedAttr
683 });
684 key = val = '';
685 loc = 'key';
686 escapedAttr = false;
687 } else {
688 switch (loc) {
689 case 'key-char':
690 if (str[i] === quote) {
691 loc = 'key';
692 if (i + 1 < str.length && [' ', ',', '!', '=', '\n'].indexOf(str[i + 1]) === -1)
693 throw new Error('Unexpected character ' + str[i + 1] + ' expected ` `, `\\n`, `,`, `!` or `=`');
694 } else {
695 key += str[i];
696 }
697 break;
698 case 'key':
699 if (key === '' && (str[i] === '"' || str[i] === "'")) {
700 loc = 'key-char';
701 quote = str[i];
702 } else if (str[i] === '!' || str[i] === '=') {
703 escapedAttr = str[i] !== '!';
704 if (str[i] === '!') i++;
705 if (str[i] !== '=') throw new Error('Unexpected character ' + str[i] + ' expected `=`');
706 loc = 'value';
707 state = characterParser.defaultState();
708 } else {
709 key += str[i]
710 }
711 break;
712 case 'value':
713 state = characterParser.parseChar(str[i], state);
714 if (state.isString()) {
715 loc = 'string';
716 quote = str[i];
717 interpolatable = str[i];
718 } else {
719 val += str[i];
720 }
721 break;
722 case 'string':
723 state = characterParser.parseChar(str[i], state);
724 interpolatable += str[i];
725 if (!state.isString()) {
726 loc = 'value';
727 val += interpolate(interpolatable);
728 }
729 break;
730 }
731 }
732 }
733
734 if ('/' == this.input.charAt(0)) {
735 this.consume(1);
736 tok.selfClosing = true;
737 }
738
739 return tok;
740 }
741 },
742
743 /**
744 * &attributes block
745 */
746 attributesBlock: function () {
747 var captures;
748 if (/^&attributes\b/.test(this.input)) {
749 this.consume(11);
750 var args = this.bracketExpression();
751 this.consume(args.end + 1);
752 return this.tok('&attributes', args.src);
753 }
754 },
755
756 /**
757 * Indent | Outdent | Newline.
758 */
759
760 indent: function() {
761 var captures, re;
762
763 // established regexp
764 if (this.indentRe) {
765 captures = this.indentRe.exec(this.input);
766 // determine regexp
767 } else {
768 // tabs
769 re = /^\n(\t*) */;
770 captures = re.exec(this.input);
771
772 // spaces
773 if (captures && !captures[1].length) {
774 re = /^\n( *)/;
775 captures = re.exec(this.input);
776 }
777
778 // established
779 if (captures && captures[1].length) this.indentRe = re;
780 }
781
782 if (captures) {
783 var tok
784 , indents = captures[1].length;
785
786 ++this.lineno;
787 this.consume(indents + 1);
788
789 if (' ' == this.input[0] || '\t' == this.input[0]) {
790 throw new Error('Invalid indentation, you can use tabs or spaces but not both');
791 }
792
793 // blank line
794 if ('\n' == this.input[0]) {
795 this.pipeless = false;
796 return this.tok('newline');
797 }
798
799 // outdent
800 if (this.indentStack.length && indents < this.indentStack[0]) {
801 while (this.indentStack.length && this.indentStack[0] > indents) {
802 this.stash.push(this.tok('outdent'));
803 this.indentStack.shift();
804 }
805 tok = this.stash.pop();
806 // indent
807 } else if (indents && indents != this.indentStack[0]) {
808 this.indentStack.unshift(indents);
809 tok = this.tok('indent', indents);
810 // newline
811 } else {
812 tok = this.tok('newline');
813 }
814
815 this.pipeless = false;
816 return tok;
817 }
818 },
819
820 /**
821 * Pipe-less text consumed only when
822 * pipeless is true;
823 */
824
825 pipelessText: function() {
826 if (!this.pipeless) return;
827 var captures, re;
828
829 // established regexp
830 if (this.indentRe) {
831 captures = this.indentRe.exec(this.input);
832 // determine regexp
833 } else {
834 // tabs
835 re = /^\n(\t*) */;
836 captures = re.exec(this.input);
837
838 // spaces
839 if (captures && !captures[1].length) {
840 re = /^\n( *)/;
841 captures = re.exec(this.input);
842 }
843
844 // established
845 if (captures && captures[1].length) this.indentRe = re;
846 }
847
848 var indents = captures && captures[1].length;
849 if (indents && (this.indentStack.length === 0 || indents > this.indentStack[0])) {
850 var indent = captures[1];
851 var line;
852 var tokens = [];
853 var isMatch;
854 do {
855 // text has `\n` as a prefix
856 var i = this.input.substr(1).indexOf('\n');
857 if (-1 == i) i = this.input.length - 1;
858 var str = this.input.substr(1, i);
859 isMatch = str.substr(0, indent.length) === indent || !str.trim();
860 if (isMatch) {
861 // consume test along with `\n` prefix if match
862 this.consume(str.length + 1);
863 ++this.lineno;
864 tokens.push(str.substr(indent.length));
865 }
866 } while(this.input.length && isMatch);
867 while (this.input.length === 0 && tokens[tokens.length - 1] === '') tokens.pop();
868 return this.tok('pipeless-text', tokens);
869 }
870 },
871
872 /**
873 * ':'
874 */
875
876 colon: function() {
877 var good = /^: +/.test(this.input);
878 var res = this.scan(/^: */, ':');
879 if (res && !good) {
880 console.warn('Warning: space required after `:` on line ' + this.lineno +
881 ' of jade file "' + this.filename + '"');
882 }
883 return res;
884 },
885
886 fail: function () {
887 throw new Error('unexpected text ' + this.input.substr(0, 5));
888 },
889
890 /**
891 * Return the next token object, or those
892 * previously stashed by lookahead.
893 *
894 * @return {Object}
895 * @api private
896 */
897
898 advance: function(){
899 return this.stashed()
900 || this.next();
901 },
902
903 /**
904 * Return the next token object.
905 *
906 * @return {Object}
907 * @api private
908 */
909
910 next: function() {
911 return this.deferred()
912 || this.blank()
913 || this.eos()
914 || this.pipelessText()
915 || this.yield()
916 || this.doctype()
917 || this.interpolation()
918 || this["case"]()
919 || this.when()
920 || this["default"]()
921 || this["extends"]()
922 || this.append()
923 || this.prepend()
924 || this.block()
925 || this.mixinBlock()
926 || this.include()
927 || this.includeFiltered()
928 || this.mixin()
929 || this.call()
930 || this.conditional()
931 || this.each()
932 || this["while"]()
933 || this.tag()
934 || this.filter()
935 || this.blockCode()
936 || this.code()
937 || this.id()
938 || this.className()
939 || this.attrs()
940 || this.attributesBlock()
941 || this.indent()
942 || this.text()
943 || this.comment()
944 || this.colon()
945 || this.dot()
946 || this.textFail()
947 || this.fail();
948 }
949};