UNPKG

18.3 kBJavaScriptView Raw
1
2/*!
3 * Stylus - Lexer
4 * Copyright(c) 2010 LearnBoost <dev@learnboost.com>
5 * MIT Licensed
6 */
7
8/**
9 * Module dependencies.
10 */
11
12var Token = require('./token')
13 , nodes = require('./nodes')
14 , errors = require('./errors')
15 , units = require('./units');
16
17/**
18 * Expose `Lexer`.
19 */
20
21exports = module.exports = Lexer;
22
23/**
24 * Operator aliases.
25 */
26
27var alias = {
28 'and': '&&'
29 , 'or': '||'
30 , 'is': '=='
31 , 'isnt': '!='
32 , 'is not': '!='
33 , ':=': '?='
34};
35
36/**
37 * Units.
38 */
39
40units = units.join('|');
41
42/**
43 * Unit RegExp.
44 */
45
46var unit = new RegExp('^(-)?(\\d+\\.\\d+|\\d+|\\.\\d+)(' + units + ')?[ \\t]*');
47
48/**
49 * Initialize a new `Lexer` with the given `str` and `options`.
50 *
51 * @param {String} str
52 * @param {Object} options
53 * @api private
54 */
55
56function Lexer(str, options) {
57 options = options || {};
58 this.stash = [];
59 this.indentStack = [];
60 this.indentRe = null;
61 this.lineno = 1;
62 this.column = 1;
63
64 // HACK!
65 function comment(str, val, offset, s) {
66 var inComment = s.lastIndexOf('/*', offset) > s.lastIndexOf('*/', offset)
67 , commentIdx = s.lastIndexOf('//', offset)
68 , i = s.lastIndexOf('\n', offset)
69 , double = 0
70 , single = 0;
71
72 if (~commentIdx && commentIdx > i) {
73 while (i != offset) {
74 if ("'" == s[i]) single ? single-- : single++;
75 if ('"' == s[i]) double ? double-- : double++;
76
77 if ('/' == s[i] && '/' == s[i + 1]) {
78 inComment = !single && !double;
79 break;
80 }
81 ++i;
82 }
83 }
84
85 return inComment
86 ? str
87 : val + '\r';
88 };
89
90 // Remove UTF-8 BOM.
91 if ('\uFEFF' == str.charAt(0)) str = str.slice(1);
92
93 this.str = str
94 .replace(/\s+$/, '\n')
95 .replace(/\r\n?/g, '\n')
96 .replace(/\\ *\n/g, '\r')
97 .replace(/([,(:](?!\/\/[^ ])) *(?:\/\/[^\n]*)?\n\s*/g, comment)
98 .replace(/\s*\n[ \t]*([,)])/g, comment);
99};
100
101/**
102 * Lexer prototype.
103 */
104
105Lexer.prototype = {
106
107 /**
108 * Custom inspect.
109 */
110
111 inspect: function(){
112 var tok
113 , tmp = this.str
114 , buf = [];
115 while ('eos' != (tok = this.next()).type) {
116 buf.push(tok.inspect());
117 }
118 this.str = tmp;
119 return buf.concat(tok.inspect()).join('\n');
120 },
121
122 /**
123 * Lookahead `n` tokens.
124 *
125 * @param {Number} n
126 * @return {Object}
127 * @api private
128 */
129
130 lookahead: function(n){
131 var fetch = n - this.stash.length;
132 while (fetch-- > 0) this.stash.push(this.advance());
133 return this.stash[--n];
134 },
135
136 /**
137 * Consume the given `len`.
138 *
139 * @param {Number|Array} len
140 * @api private
141 */
142
143 skip: function(len){
144 var chunk = len[0];
145 len = chunk ? chunk.length : len;
146 this.str = this.str.substr(len);
147 if (chunk) {
148 this.move(chunk);
149 } else {
150 this.column += len;
151 }
152 },
153
154 /**
155 * Move current line and column position.
156 *
157 * @param {String} str
158 * @api private
159 */
160
161 move: function(str){
162 var lines = str.match(/\n/g)
163 , idx = str.lastIndexOf('\n');
164
165 if (lines) this.lineno += lines.length;
166 this.column = ~idx
167 ? str.length - idx
168 : this.column + str.length;
169 },
170
171 /**
172 * Fetch next token including those stashed by peek.
173 *
174 * @return {Token}
175 * @api private
176 */
177
178 next: function() {
179 var tok = this.stashed() || this.advance();
180 this.prev = tok;
181 return tok;
182 },
183
184 /**
185 * Check if the current token is a part of selector.
186 *
187 * @return {Boolean}
188 * @api private
189 */
190
191 isPartOfSelector: function() {
192 var tok = this.stash[this.stash.length - 1] || this.prev;
193 switch (tok && tok.type) {
194 // #for
195 case 'color':
196 return 2 == tok.val.raw.length;
197 // .or
198 case '.':
199 // [is]
200 case '[':
201 return true;
202 }
203 return false;
204 },
205
206 /**
207 * Fetch next token.
208 *
209 * @return {Token}
210 * @api private
211 */
212
213 advance: function() {
214 var column = this.column
215 , line = this.lineno
216 , tok = this.eos()
217 || this.null()
218 || this.sep()
219 || this.keyword()
220 || this.urlchars()
221 || this.comment()
222 || this.newline()
223 || this.escaped()
224 || this.important()
225 || this.literal()
226 || this.atrule()
227 || this.function()
228 || this.brace()
229 || this.paren()
230 || this.color()
231 || this.string()
232 || this.unit()
233 || this.namedop()
234 || this.boolean()
235 || this.unicode()
236 || this.ident()
237 || this.op()
238 || this.eol()
239 || this.space()
240 || this.selector();
241 tok.lineno = line;
242 tok.column = column;
243 return tok;
244 },
245
246 /**
247 * Lookahead a single token.
248 *
249 * @return {Token}
250 * @api private
251 */
252
253 peek: function() {
254 return this.lookahead(1);
255 },
256
257 /**
258 * Return the next possibly stashed token.
259 *
260 * @return {Token}
261 * @api private
262 */
263
264 stashed: function() {
265 return this.stash.shift();
266 },
267
268 /**
269 * EOS | trailing outdents.
270 */
271
272 eos: function() {
273 if (this.str.length) return;
274 if (this.indentStack.length) {
275 this.indentStack.shift();
276 return new Token('outdent');
277 } else {
278 return new Token('eos');
279 }
280 },
281
282 /**
283 * url char
284 */
285
286 urlchars: function() {
287 var captures;
288 if (!this.isURL) return;
289 if (captures = /^[\/:@.;?&=*!,<>#%0-9]+/.exec(this.str)) {
290 this.skip(captures);
291 return new Token('literal', new nodes.Literal(captures[0]));
292 }
293 },
294
295 /**
296 * ';' [ \t]*
297 */
298
299 sep: function() {
300 var captures;
301 if (captures = /^;[ \t]*/.exec(this.str)) {
302 this.skip(captures);
303 return new Token(';');
304 }
305 },
306
307 /**
308 * '\r'
309 */
310
311 eol: function() {
312 if ('\r' == this.str[0]) {
313 ++this.lineno;
314 this.skip(1);
315 return this.advance();
316 }
317 },
318
319 /**
320 * ' '+
321 */
322
323 space: function() {
324 var captures;
325 if (captures = /^([ \t]+)/.exec(this.str)) {
326 this.skip(captures);
327 return new Token('space');
328 }
329 },
330
331 /**
332 * '\\' . ' '*
333 */
334
335 escaped: function() {
336 var captures;
337 if (captures = /^\\(.)[ \t]*/.exec(this.str)) {
338 var c = captures[1];
339 this.skip(captures);
340 return new Token('ident', new nodes.Literal(c));
341 }
342 },
343
344 /**
345 * '@css' ' '* '{' .* '}' ' '*
346 */
347
348 literal: function() {
349 // HACK attack !!!
350 var captures;
351 if (captures = /^@css[ \t]*\{/.exec(this.str)) {
352 this.skip(captures);
353 var c
354 , braces = 1
355 , css = ''
356 , node;
357 while (c = this.str[0]) {
358 this.str = this.str.substr(1);
359 switch (c) {
360 case '{': ++braces; break;
361 case '}': --braces; break;
362 case '\n': ++this.lineno; break;
363 }
364 css += c;
365 if (!braces) break;
366 }
367 css = css.replace(/\s*}$/, '');
368 node = new nodes.Literal(css);
369 node.css = true;
370 return new Token('literal', node);
371 }
372 },
373
374 /**
375 * '!important' ' '*
376 */
377
378 important: function() {
379 var captures;
380 if (captures = /^!important[ \t]*/.exec(this.str)) {
381 this.skip(captures);
382 return new Token('ident', new nodes.Literal('!important'));
383 }
384 },
385
386 /**
387 * '{' | '}'
388 */
389
390 brace: function() {
391 var captures;
392 if (captures = /^([{}])/.exec(this.str)) {
393 this.skip(1);
394 var brace = captures[1];
395 return new Token(brace, brace);
396 }
397 },
398
399 /**
400 * '(' | ')' ' '*
401 */
402
403 paren: function() {
404 var captures;
405 if (captures = /^([()])([ \t]*)/.exec(this.str)) {
406 var paren = captures[1];
407 this.skip(captures);
408 if (')' == paren) this.isURL = false;
409 var tok = new Token(paren, paren);
410 tok.space = captures[2];
411 return tok;
412 }
413 },
414
415 /**
416 * 'null'
417 */
418
419 null: function() {
420 var captures
421 , tok;
422 if (captures = /^(null)\b[ \t]*/.exec(this.str)) {
423 this.skip(captures);
424 if (this.isPartOfSelector()) {
425 tok = new Token('ident', new nodes.Ident(captures[0]));
426 } else {
427 tok = new Token('null', nodes.null);
428 }
429 return tok;
430 }
431 },
432
433 /**
434 * 'if'
435 * | 'else'
436 * | 'unless'
437 * | 'return'
438 * | 'for'
439 * | 'in'
440 */
441
442 keyword: function() {
443 var captures
444 , tok;
445 if (captures = /^(return|if|else|unless|for|in)\b[ \t]*/.exec(this.str)) {
446 var keyword = captures[1];
447 this.skip(captures);
448 if (this.isPartOfSelector()) {
449 tok = new Token('ident', new nodes.Ident(captures[0]));
450 } else {
451 tok = new Token(keyword, keyword);
452 }
453 return tok;
454 }
455 },
456
457 /**
458 * 'not'
459 * | 'and'
460 * | 'or'
461 * | 'is'
462 * | 'is not'
463 * | 'isnt'
464 * | 'is a'
465 * | 'is defined'
466 */
467
468 namedop: function() {
469 var captures
470 , tok;
471 if (captures = /^(not|and|or|is a|is defined|isnt|is not|is)(?!-)\b([ \t]*)/.exec(this.str)) {
472 var op = captures[1];
473 this.skip(captures);
474 if (this.isPartOfSelector()) {
475 tok = new Token('ident', new nodes.Ident(captures[0]));
476 } else {
477 op = alias[op] || op;
478 tok = new Token(op, op);
479 }
480 tok.space = captures[2];
481 return tok;
482 }
483 },
484
485 /**
486 * ','
487 * | '+'
488 * | '+='
489 * | '-'
490 * | '-='
491 * | '*'
492 * | '*='
493 * | '/'
494 * | '/='
495 * | '%'
496 * | '%='
497 * | '**'
498 * | '!'
499 * | '&'
500 * | '&&'
501 * | '||'
502 * | '>'
503 * | '>='
504 * | '<'
505 * | '<='
506 * | '='
507 * | '=='
508 * | '!='
509 * | '!'
510 * | '~'
511 * | '?='
512 * | ':='
513 * | '?'
514 * | ':'
515 * | '['
516 * | ']'
517 * | '.'
518 * | '..'
519 * | '...'
520 */
521
522 op: function() {
523 var captures;
524 if (captures = /^([.]{1,3}|&&|\|\||[!<>=?:]=|\*\*|[-+*\/%]=?|[,=?:!~<>&\[\]])([ \t]*)/.exec(this.str)) {
525 var op = captures[1];
526 this.skip(captures);
527 op = alias[op] || op;
528 var tok = new Token(op, op);
529 tok.space = captures[2];
530 this.isURL = false;
531 return tok;
532 }
533 },
534
535 /**
536 * '@' (-(\w+)-)?[a-zA-Z0-9-_]*(?
537 */
538
539 atrule: function() {
540 var captures;
541 if (captures = /^@(?:-(\w+)-)?([a-zA-Z0-9-_]*\(?)[ \t]*/.exec(this.str)) {
542 this.skip(captures);
543 var vendor = captures[1]
544 , type = captures[2]
545 , tok;
546 switch (type) {
547 case 'require':
548 case 'import':
549 case 'charset':
550 case 'namespace':
551 case 'media':
552 case 'scope':
553 return new Token(type);
554 case 'document':
555 return new Token('-moz-document');
556 case 'block':
557 return new Token('atblock');
558 case '(':
559 tok = new Token('function', new nodes.Ident('anonymous'));
560 tok.anonymous = true;
561 return tok;
562 case 'extend':
563 case 'extends':
564 return new Token('extend');
565 case 'keyframes':
566 return new Token(type, vendor);
567 default:
568 return new Token('atrule', (vendor ? '-' + vendor + '-' + type : type));
569 }
570 }
571 },
572
573 /**
574 * '//' *
575 */
576
577 comment: function() {
578 // Single line
579 if ('/' == this.str[0] && '/' == this.str[1]) {
580 var end = this.str.indexOf('\n');
581 if (-1 == end) end = this.str.length;
582 this.skip(end);
583 return this.advance();
584 }
585
586 // Multi-line
587 if ('/' == this.str[0] && '*' == this.str[1]) {
588 var end = this.str.indexOf('*/');
589 if (-1 == end) end = this.str.length;
590 var str = this.str.substr(0, end + 2)
591 , lines = str.split('\n').length - 1
592 , suppress = true
593 , inline = false;
594 this.lineno += lines;
595 this.skip(end + 2);
596 // output
597 if ('!' == str[2]) {
598 str = str.replace('*!', '*');
599 suppress = false;
600 }
601 if (this.prev && ';' == this.prev.type) inline = true;
602 return new Token('comment', new nodes.Comment(str, suppress, inline));
603 }
604 },
605
606 /**
607 * 'true' | 'false'
608 */
609
610 boolean: function() {
611 var captures;
612 if (captures = /^(true|false)\b([ \t]*)/.exec(this.str)) {
613 var val = nodes.Boolean('true' == captures[1]);
614 this.skip(captures);
615 var tok = new Token('boolean', val);
616 tok.space = captures[2];
617 return tok;
618 }
619 },
620
621 /**
622 * 'U+' [0-9A-Fa-f?]{1,6}(?:-[0-9A-Fa-f]{1,6})?
623 */
624
625 unicode: function() {
626 var captures;
627 if (captures = /^u\+[0-9a-f?]{1,6}(?:-[0-9a-f]{1,6})?/i.exec(this.str)) {
628 this.skip(captures);
629 return new Token('literal', new nodes.Literal(captures[0]));
630 }
631 },
632
633 /**
634 * -*[_a-zA-Z$] [-\w\d$]* '('
635 */
636
637 function: function() {
638 var captures;
639 if (captures = /^(-*[_a-zA-Z$][-\w\d$]*)\(([ \t]*)/.exec(this.str)) {
640 var name = captures[1];
641 this.skip(captures);
642 this.isURL = 'url' == name;
643 var tok = new Token('function', new nodes.Ident(name));
644 tok.space = captures[2];
645 return tok;
646 }
647 },
648
649 /**
650 * -*[_a-zA-Z$] [-\w\d$]*
651 */
652
653 ident: function() {
654 var captures;
655 if (captures = /^-*[_a-zA-Z$][-\w\d$]*/.exec(this.str)) {
656 this.skip(captures);
657 return new Token('ident', new nodes.Ident(captures[0]));
658 }
659 },
660
661 /**
662 * '\n' ' '+
663 */
664
665 newline: function() {
666 var captures, re;
667
668 // we have established the indentation regexp
669 if (this.indentRe){
670 captures = this.indentRe.exec(this.str);
671 // figure out if we are using tabs or spaces
672 } else {
673 // try tabs
674 re = /^\n([\t]*)[ \t]*/;
675 captures = re.exec(this.str);
676
677 // nope, try spaces
678 if (captures && !captures[1].length) {
679 re = /^\n([ \t]*)/;
680 captures = re.exec(this.str);
681 }
682
683 // established
684 if (captures && captures[1].length) this.indentRe = re;
685 }
686
687
688 if (captures) {
689 var tok
690 , indents = captures[1].length;
691
692 this.skip(captures);
693 if (this.str[0] === ' ' || this.str[0] === '\t') {
694 throw new errors.SyntaxError('Invalid indentation. You can use tabs or spaces to indent, but not both.');
695 }
696
697 // Blank line
698 if ('\n' == this.str[0]) return this.advance();
699
700 // Outdent
701 if (this.indentStack.length && indents < this.indentStack[0]) {
702 while (this.indentStack.length && this.indentStack[0] > indents) {
703 this.stash.push(new Token('outdent'));
704 this.indentStack.shift();
705 }
706 tok = this.stash.pop();
707 // Indent
708 } else if (indents && indents != this.indentStack[0]) {
709 this.indentStack.unshift(indents);
710 tok = new Token('indent');
711 // Newline
712 } else {
713 tok = new Token('newline');
714 }
715
716 return tok;
717 }
718 },
719
720 /**
721 * '-'? (digit+ | digit* '.' digit+) unit
722 */
723
724 unit: function() {
725 var captures;
726 if (captures = unit.exec(this.str)) {
727 this.skip(captures);
728 var n = parseFloat(captures[2]);
729 if ('-' == captures[1]) n = -n;
730 var node = new nodes.Unit(n, captures[3]);
731 node.raw = captures[0];
732 return new Token('unit', node);
733 }
734 },
735
736 /**
737 * '"' [^"]+ '"' | "'"" [^']+ "'"
738 */
739
740 string: function() {
741 var captures;
742 if (captures = /^("[^"]*"|'[^']*')[ \t]*/.exec(this.str)) {
743 var str = captures[1]
744 , quote = captures[0][0];
745 this.skip(captures);
746 str = str.slice(1,-1).replace(/\\n/g, '\n');
747 return new Token('string', new nodes.String(str, quote));
748 }
749 },
750
751 /**
752 * #rrggbbaa | #rrggbb | #rgba | #rgb | #nn | #n
753 */
754
755 color: function() {
756 return this.rrggbbaa()
757 || this.rrggbb()
758 || this.rgba()
759 || this.rgb()
760 || this.nn()
761 || this.n()
762 },
763
764 /**
765 * #n
766 */
767
768 n: function() {
769 var captures;
770 if (captures = /^#([a-fA-F0-9]{1})[ \t]*/.exec(this.str)) {
771 this.skip(captures);
772 var n = parseInt(captures[1] + captures[1], 16)
773 , color = new nodes.RGBA(n, n, n, 1);
774 color.raw = captures[0];
775 return new Token('color', color);
776 }
777 },
778
779 /**
780 * #nn
781 */
782
783 nn: function() {
784 var captures;
785 if (captures = /^#([a-fA-F0-9]{2})[ \t]*/.exec(this.str)) {
786 this.skip(captures);
787 var n = parseInt(captures[1], 16)
788 , color = new nodes.RGBA(n, n, n, 1);
789 color.raw = captures[0];
790 return new Token('color', color);
791 }
792 },
793
794 /**
795 * #rgb
796 */
797
798 rgb: function() {
799 var captures;
800 if (captures = /^#([a-fA-F0-9]{3})[ \t]*/.exec(this.str)) {
801 this.skip(captures);
802 var rgb = captures[1]
803 , r = parseInt(rgb[0] + rgb[0], 16)
804 , g = parseInt(rgb[1] + rgb[1], 16)
805 , b = parseInt(rgb[2] + rgb[2], 16)
806 , color = new nodes.RGBA(r, g, b, 1);
807 color.raw = captures[0];
808 return new Token('color', color);
809 }
810 },
811
812 /**
813 * #rgba
814 */
815
816 rgba: function() {
817 var captures;
818 if (captures = /^#([a-fA-F0-9]{4})[ \t]*/.exec(this.str)) {
819 this.skip(captures);
820 var rgb = captures[1]
821 , r = parseInt(rgb[0] + rgb[0], 16)
822 , g = parseInt(rgb[1] + rgb[1], 16)
823 , b = parseInt(rgb[2] + rgb[2], 16)
824 , a = parseInt(rgb[3] + rgb[3], 16)
825 , color = new nodes.RGBA(r, g, b, a/255);
826 color.raw = captures[0];
827 return new Token('color', color);
828 }
829 },
830
831 /**
832 * #rrggbb
833 */
834
835 rrggbb: function() {
836 var captures;
837 if (captures = /^#([a-fA-F0-9]{6})[ \t]*/.exec(this.str)) {
838 this.skip(captures);
839 var rgb = captures[1]
840 , r = parseInt(rgb.substr(0, 2), 16)
841 , g = parseInt(rgb.substr(2, 2), 16)
842 , b = parseInt(rgb.substr(4, 2), 16)
843 , color = new nodes.RGBA(r, g, b, 1);
844 color.raw = captures[0];
845 return new Token('color', color);
846 }
847 },
848
849 /**
850 * #rrggbbaa
851 */
852
853 rrggbbaa: function() {
854 var captures;
855 if (captures = /^#([a-fA-F0-9]{8})[ \t]*/.exec(this.str)) {
856 this.skip(captures);
857 var rgb = captures[1]
858 , r = parseInt(rgb.substr(0, 2), 16)
859 , g = parseInt(rgb.substr(2, 2), 16)
860 , b = parseInt(rgb.substr(4, 2), 16)
861 , a = parseInt(rgb.substr(6, 2), 16)
862 , color = new nodes.RGBA(r, g, b, a/255);
863 color.raw = captures[0];
864 return new Token('color', color);
865 }
866 },
867
868 /**
869 * [^\n,;]+
870 */
871
872 selector: function() {
873 var captures;
874 if (captures = /^.*?(?=\/\/(?![^\[]*\])|[,\n{])/.exec(this.str)) {
875 var selector = captures[0];
876 this.skip(captures);
877 return new Token('selector', selector);
878 }
879 }
880};