UNPKG

21.9 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;
202 try {
203 match = this.bracketExpression(1);
204 } catch (ex) {
205 return;//not an interpolation expression, just an unmatched open interpolation
206 }
207
208 this.consume(match.end + 1);
209 return this.tok('interpolation', match.src);
210 }
211 },
212
213 /**
214 * Tag.
215 */
216
217 tag: function() {
218 var captures;
219 if (captures = /^(\w[-:\w]*)(\/?)/.exec(this.input)) {
220 this.consume(captures[0].length);
221 var tok, name = captures[1];
222 if (':' == name[name.length - 1]) {
223 name = name.slice(0, -1);
224 tok = this.tok('tag', name);
225 this.defer(this.tok(':'));
226 while (' ' == this.input[0]) this.input = this.input.substr(1);
227 } else {
228 tok = this.tok('tag', name);
229 }
230 tok.selfClosing = !!captures[2];
231 return tok;
232 }
233 },
234
235 /**
236 * Filter.
237 */
238
239 filter: function() {
240 var tok = this.scan(/^:([\w\-]+)/, 'filter');
241 if (tok) {
242 this.pipeless = true;
243 return tok;
244 }
245 },
246
247 /**
248 * Doctype.
249 */
250
251 doctype: function() {
252 if (this.scan(/^!!! *([^\n]+)?/, 'doctype')) {
253 throw new Error('`!!!` is deprecated, you must now use `doctype`');
254 }
255 var node = this.scan(/^(?:doctype) *([^\n]+)?/, 'doctype');
256 if (node && node.val && node.val.trim() === '5') {
257 throw new Error('`doctype 5` is deprecated, you must now use `doctype html`');
258 }
259 return node;
260 },
261
262 /**
263 * Id.
264 */
265
266 id: function() {
267 return this.scan(/^#([\w-]+)/, 'id');
268 },
269
270 /**
271 * Class.
272 */
273
274 className: function() {
275 return this.scan(/^\.([\w-]+)/, 'class');
276 },
277
278 /**
279 * Text.
280 */
281
282 text: function() {
283 return this.scan(/^(?:\| ?| )([^\n]+)/, 'text') ||
284 this.scan(/^\|?( )/, 'text') ||
285 this.scan(/^(<[^\n]*)/, 'text');
286 },
287
288 textFail: function () {
289 var tok;
290 if (tok = this.scan(/^([^\.\n][^\n]+)/, 'text')) {
291 console.warn('Warning: missing space before text for line ' + this.lineno +
292 ' of jade file "' + this.filename + '"');
293 return tok;
294 }
295 },
296
297 /**
298 * Dot.
299 */
300
301 dot: function() {
302 var match;
303 if (match = this.scan(/^\./, 'dot')) {
304 this.pipeless = true;
305 return match;
306 }
307 },
308
309 /**
310 * Extends.
311 */
312
313 "extends": function() {
314 return this.scan(/^extends? +([^\n]+)/, 'extends');
315 },
316
317 /**
318 * Block prepend.
319 */
320
321 prepend: function() {
322 var captures;
323 if (captures = /^prepend +([^\n]+)/.exec(this.input)) {
324 this.consume(captures[0].length);
325 var mode = 'prepend'
326 , name = captures[1]
327 , tok = this.tok('block', name);
328 tok.mode = mode;
329 return tok;
330 }
331 },
332
333 /**
334 * Block append.
335 */
336
337 append: function() {
338 var captures;
339 if (captures = /^append +([^\n]+)/.exec(this.input)) {
340 this.consume(captures[0].length);
341 var mode = 'append'
342 , name = captures[1]
343 , tok = this.tok('block', name);
344 tok.mode = mode;
345 return tok;
346 }
347 },
348
349 /**
350 * Block.
351 */
352
353 block: function() {
354 var captures;
355 if (captures = /^block\b *(?:(prepend|append) +)?([^\n]+)/.exec(this.input)) {
356 this.consume(captures[0].length);
357 var mode = captures[1] || 'replace'
358 , name = captures[2]
359 , tok = this.tok('block', name);
360
361 tok.mode = mode;
362 return tok;
363 }
364 },
365
366 /**
367 * Mixin Block.
368 */
369
370 mixinBlock: function() {
371 var captures;
372 if (captures = /^block\s*(\n|$)/.exec(this.input)) {
373 this.consume(captures[0].length - 1);
374 return this.tok('mixin-block');
375 }
376 },
377
378 /**
379 * Yield.
380 */
381
382 'yield': function() {
383 return this.scan(/^yield */, 'yield');
384 },
385
386 /**
387 * Include.
388 */
389
390 include: function() {
391 return this.scan(/^include +([^\n]+)/, 'include');
392 },
393
394 /**
395 * Include with filter
396 */
397
398 includeFiltered: function() {
399 var captures;
400 if (captures = /^include:([\w\-]+)([\( ])/.exec(this.input)) {
401 this.consume(captures[0].length - 1);
402 var filter = captures[1];
403 var attrs = captures[2] === '(' ? this.attrs() : null;
404 if (!(captures[2] === ' ' || this.input[0] === ' ')) {
405 throw new Error('expected space after include:filter but got ' + JSON.stringify(this.input[0]));
406 }
407 captures = /^ *([^\n]+)/.exec(this.input);
408 if (!captures || captures[1].trim() === '') {
409 throw new Error('missing path for include:filter');
410 }
411 this.consume(captures[0].length);
412 var path = captures[1];
413 var tok = this.tok('include', path);
414 tok.filter = filter;
415 tok.attrs = attrs;
416 return tok;
417 }
418 },
419
420 /**
421 * Case.
422 */
423
424 "case": function() {
425 return this.scan(/^case +([^\n]+)/, 'case');
426 },
427
428 /**
429 * When.
430 */
431
432 when: function() {
433 return this.scan(/^when +([^:\n]+)/, 'when');
434 },
435
436 /**
437 * Default.
438 */
439
440 "default": function() {
441 return this.scan(/^default */, 'default');
442 },
443
444 /**
445 * Call mixin.
446 */
447
448 call: function(){
449
450 var tok, captures;
451 if (captures = /^\+(([-\w]+)|(#\{))/.exec(this.input)) {
452 // try to consume simple or interpolated call
453 if (captures[2]) {
454 // simple call
455 this.consume(captures[0].length);
456 tok = this.tok('call', captures[2]);
457 } else {
458 // interpolated call
459 var match;
460 try {
461 match = this.bracketExpression(2);
462 } catch (ex) {
463 return;//not an interpolation expression, just an unmatched open interpolation
464 }
465 this.consume(match.end + 1);
466 assertExpression(match.src);
467 tok = this.tok('call', '#{'+match.src+'}');
468 }
469
470 // Check for args (not attributes)
471 if (captures = /^ *\(/.exec(this.input)) {
472 try {
473 var range = this.bracketExpression(captures[0].length - 1);
474 if (!/^\s*[-\w]+ *=/.test(range.src)) { // not attributes
475 this.consume(range.end + 1);
476 tok.args = range.src;
477 }
478 } catch (ex) {
479 //not a bracket expcetion, just unmatched open parens
480 }
481 }
482
483 return tok;
484 }
485 },
486
487 /**
488 * Mixin.
489 */
490
491 mixin: function(){
492 var captures;
493 if (captures = /^mixin +([-\w]+)(?: *\((.*)\))? */.exec(this.input)) {
494 this.consume(captures[0].length);
495 var tok = this.tok('mixin', captures[1]);
496 tok.args = captures[2];
497 return tok;
498 }
499 },
500
501 /**
502 * Conditional.
503 */
504
505 conditional: function() {
506 var captures;
507 if (captures = /^(if|unless|else if|else)\b([^\n]*)/.exec(this.input)) {
508 this.consume(captures[0].length);
509 var type = captures[1]
510 var js = captures[2];
511 var isIf = false;
512 var isElse = false;
513
514 switch (type) {
515 case 'if':
516 assertExpression(js)
517 js = 'if (' + js + ')';
518 isIf = true;
519 break;
520 case 'unless':
521 assertExpression(js)
522 js = 'if (!(' + js + '))';
523 isIf = true;
524 break;
525 case 'else if':
526 assertExpression(js)
527 js = 'else if (' + js + ')';
528 isIf = true;
529 isElse = true;
530 break;
531 case 'else':
532 if (js && js.trim()) {
533 throw new Error('`else` cannot have a condition, perhaps you meant `else if`');
534 }
535 js = 'else';
536 isElse = true;
537 break;
538 }
539 var tok = this.tok('code', js);
540 tok.isElse = isElse;
541 tok.isIf = isIf;
542 tok.requiresBlock = true;
543 return tok;
544 }
545 },
546
547 /**
548 * While.
549 */
550
551 "while": function() {
552 var captures;
553 if (captures = /^while +([^\n]+)/.exec(this.input)) {
554 this.consume(captures[0].length);
555 assertExpression(captures[1])
556 var tok = this.tok('code', 'while (' + captures[1] + ')');
557 tok.requiresBlock = true;
558 return tok;
559 }
560 },
561
562 /**
563 * Each.
564 */
565
566 each: function() {
567 var captures;
568 if (captures = /^(?:- *)?(?:each|for) +([a-zA-Z_$][\w$]*)(?: *, *([a-zA-Z_$][\w$]*))? * in *([^\n]+)/.exec(this.input)) {
569 this.consume(captures[0].length);
570 var tok = this.tok('each', captures[1]);
571 tok.key = captures[2] || '$index';
572 assertExpression(captures[3])
573 tok.code = captures[3];
574 return tok;
575 }
576 },
577
578 /**
579 * Code.
580 */
581
582 code: function() {
583 var captures;
584 if (captures = /^(!?=|-)[ \t]*([^\n]+)/.exec(this.input)) {
585 this.consume(captures[0].length);
586 var flags = captures[1];
587 captures[1] = captures[2];
588 var tok = this.tok('code', captures[1]);
589 tok.escape = flags.charAt(0) === '=';
590 tok.buffer = flags.charAt(0) === '=' || flags.charAt(1) === '=';
591 if (tok.buffer) assertExpression(captures[1])
592 return tok;
593 }
594 },
595
596 /**
597 * Attributes.
598 */
599
600 attrs: function() {
601 if ('(' == this.input.charAt(0)) {
602 var index = this.bracketExpression().end
603 , str = this.input.substr(1, index-1)
604 , tok = this.tok('attrs');
605
606 assertNestingCorrect(str);
607
608 var quote = '';
609 var interpolate = function (attr) {
610 return attr.replace(/(\\)?#\{(.+)/g, function(_, escape, expr){
611 if (escape) return _;
612 try {
613 var range = characterParser.parseMax(expr);
614 if (expr[range.end] !== '}') return _.substr(0, 2) + interpolate(_.substr(2));
615 assertExpression(range.src)
616 return quote + " + (" + range.src + ") + " + quote + interpolate(expr.substr(range.end + 1));
617 } catch (ex) {
618 return _.substr(0, 2) + interpolate(_.substr(2));
619 }
620 });
621 }
622
623 this.consume(index + 1);
624 tok.attrs = [];
625
626 var escapedAttr = true
627 var key = '';
628 var val = '';
629 var interpolatable = '';
630 var state = characterParser.defaultState();
631 var loc = 'key';
632 var isEndOfAttribute = function (i) {
633 if (key.trim() === '') return false;
634 if (i === str.length) return true;
635 if (loc === 'key') {
636 if (str[i] === ' ' || str[i] === '\n') {
637 for (var x = i; x < str.length; x++) {
638 if (str[x] != ' ' && str[x] != '\n') {
639 if (str[x] === '=' || str[x] === '!' || str[x] === ',') return false;
640 else return true;
641 }
642 }
643 }
644 return str[i] === ','
645 } else if (loc === 'value' && !state.isNesting()) {
646 try {
647 Function('', 'return (' + val + ');');
648 if (str[i] === ' ' || str[i] === '\n') {
649 for (var x = i; x < str.length; x++) {
650 if (str[x] != ' ' && str[x] != '\n') {
651 if (characterParser.isPunctuator(str[x]) && str[x] != '"' && str[x] != "'") return false;
652 else return true;
653 }
654 }
655 }
656 return str[i] === ',';
657 } catch (ex) {
658 return false;
659 }
660 }
661 }
662
663 this.lineno += str.split("\n").length - 1;
664
665 for (var i = 0; i <= str.length; i++) {
666 if (isEndOfAttribute(i)) {
667 val = val.trim();
668 if (val) assertExpression(val)
669 key = key.trim();
670 key = key.replace(/^['"]|['"]$/g, '');
671 tok.attrs.push({
672 name: key,
673 val: '' == val ? true : val,
674 escaped: escapedAttr
675 });
676 key = val = '';
677 loc = 'key';
678 escapedAttr = false;
679 } else {
680 switch (loc) {
681 case 'key-char':
682 if (str[i] === quote) {
683 loc = 'key';
684 if (i + 1 < str.length && [' ', ',', '!', '=', '\n'].indexOf(str[i + 1]) === -1)
685 throw new Error('Unexpected character ' + str[i + 1] + ' expected ` `, `\\n`, `,`, `!` or `=`');
686 } else {
687 key += str[i];
688 }
689 break;
690 case 'key':
691 if (key === '' && (str[i] === '"' || str[i] === "'")) {
692 loc = 'key-char';
693 quote = str[i];
694 } else if (str[i] === '!' || str[i] === '=') {
695 escapedAttr = str[i] !== '!';
696 if (str[i] === '!') i++;
697 if (str[i] !== '=') throw new Error('Unexpected character ' + str[i] + ' expected `=`');
698 loc = 'value';
699 state = characterParser.defaultState();
700 } else {
701 key += str[i]
702 }
703 break;
704 case 'value':
705 state = characterParser.parseChar(str[i], state);
706 if (state.isString()) {
707 loc = 'string';
708 quote = str[i];
709 interpolatable = str[i];
710 } else {
711 val += str[i];
712 }
713 break;
714 case 'string':
715 state = characterParser.parseChar(str[i], state);
716 interpolatable += str[i];
717 if (!state.isString()) {
718 loc = 'value';
719 val += interpolate(interpolatable);
720 }
721 break;
722 }
723 }
724 }
725
726 if ('/' == this.input.charAt(0)) {
727 this.consume(1);
728 tok.selfClosing = true;
729 }
730
731 return tok;
732 }
733 },
734
735 /**
736 * &attributes block
737 */
738 attributesBlock: function () {
739 var captures;
740 if (/^&attributes\b/.test(this.input)) {
741 this.consume(11);
742 var args = this.bracketExpression();
743 this.consume(args.end + 1);
744 return this.tok('&attributes', args.src);
745 }
746 },
747
748 /**
749 * Indent | Outdent | Newline.
750 */
751
752 indent: function() {
753 var captures, re;
754
755 // established regexp
756 if (this.indentRe) {
757 captures = this.indentRe.exec(this.input);
758 // determine regexp
759 } else {
760 // tabs
761 re = /^\n(\t*) */;
762 captures = re.exec(this.input);
763
764 // spaces
765 if (captures && !captures[1].length) {
766 re = /^\n( *)/;
767 captures = re.exec(this.input);
768 }
769
770 // established
771 if (captures && captures[1].length) this.indentRe = re;
772 }
773
774 if (captures) {
775 var tok
776 , indents = captures[1].length;
777
778 ++this.lineno;
779 this.consume(indents + 1);
780
781 if (' ' == this.input[0] || '\t' == this.input[0]) {
782 throw new Error('Invalid indentation, you can use tabs or spaces but not both');
783 }
784
785 // blank line
786 if ('\n' == this.input[0]) {
787 this.pipeless = false;
788 return this.tok('newline');
789 }
790
791 // outdent
792 if (this.indentStack.length && indents < this.indentStack[0]) {
793 while (this.indentStack.length && this.indentStack[0] > indents) {
794 this.stash.push(this.tok('outdent'));
795 this.indentStack.shift();
796 }
797 tok = this.stash.pop();
798 // indent
799 } else if (indents && indents != this.indentStack[0]) {
800 this.indentStack.unshift(indents);
801 tok = this.tok('indent', indents);
802 // newline
803 } else {
804 tok = this.tok('newline');
805 }
806
807 this.pipeless = false;
808 return tok;
809 }
810 },
811
812 /**
813 * Pipe-less text consumed only when
814 * pipeless is true;
815 */
816
817 pipelessText: function() {
818 if (!this.pipeless) return;
819 var captures, re;
820
821 // established regexp
822 if (this.indentRe) {
823 captures = this.indentRe.exec(this.input);
824 // determine regexp
825 } else {
826 // tabs
827 re = /^\n(\t*) */;
828 captures = re.exec(this.input);
829
830 // spaces
831 if (captures && !captures[1].length) {
832 re = /^\n( *)/;
833 captures = re.exec(this.input);
834 }
835
836 // established
837 if (captures && captures[1].length) this.indentRe = re;
838 }
839
840 var indents = captures && captures[1].length;
841 if (indents && (this.indentStack.length === 0 || indents > this.indentStack[0])) {
842 var indent = captures[1];
843 var line;
844 var tokens = [];
845 var isMatch;
846 do {
847 // text has `\n` as a prefix
848 var i = this.input.substr(1).indexOf('\n');
849 if (-1 == i) i = this.input.length - 1;
850 var str = this.input.substr(1, i);
851 isMatch = str.substr(0, indent.length) === indent || !str.trim();
852 if (isMatch) {
853 // consume test along with `\n` prefix if match
854 this.consume(str.length + 1);
855 tokens.push(str.substr(indent.length));
856 }
857 } while(this.input.length && isMatch);
858 while (this.input.length === 0 && tokens[tokens.length - 1] === '') tokens.pop();
859 return this.tok('pipeless-text', tokens);
860 }
861 },
862
863 /**
864 * ':'
865 */
866
867 colon: function() {
868 return this.scan(/^: */, ':');
869 },
870
871 fail: function () {
872 throw new Error('unexpected text ' + this.input.substr(0, 5));
873 },
874
875 /**
876 * Return the next token object, or those
877 * previously stashed by lookahead.
878 *
879 * @return {Object}
880 * @api private
881 */
882
883 advance: function(){
884 return this.stashed()
885 || this.next();
886 },
887
888 /**
889 * Return the next token object.
890 *
891 * @return {Object}
892 * @api private
893 */
894
895 next: function() {
896 return this.deferred()
897 || this.blank()
898 || this.eos()
899 || this.pipelessText()
900 || this.yield()
901 || this.doctype()
902 || this.interpolation()
903 || this["case"]()
904 || this.when()
905 || this["default"]()
906 || this["extends"]()
907 || this.append()
908 || this.prepend()
909 || this.block()
910 || this.mixinBlock()
911 || this.include()
912 || this.includeFiltered()
913 || this.mixin()
914 || this.call()
915 || this.conditional()
916 || this.each()
917 || this["while"]()
918 || this.tag()
919 || this.filter()
920 || this.code()
921 || this.id()
922 || this.className()
923 || this.attrs()
924 || this.attributesBlock()
925 || this.indent()
926 || this.text()
927 || this.comment()
928 || this.colon()
929 || this.dot()
930 || this.textFail()
931 || this.fail();
932 }
933};