UNPKG

18.4 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.anonFunc()
227 || this.atrule()
228 || this.function()
229 || this.brace()
230 || this.paren()
231 || this.color()
232 || this.string()
233 || this.unit()
234 || this.namedop()
235 || this.boolean()
236 || this.unicode()
237 || this.ident()
238 || this.op()
239 || this.eol()
240 || this.space()
241 || this.selector();
242 tok.lineno = line;
243 tok.column = column;
244 return tok;
245 },
246
247 /**
248 * Lookahead a single token.
249 *
250 * @return {Token}
251 * @api private
252 */
253
254 peek: function() {
255 return this.lookahead(1);
256 },
257
258 /**
259 * Return the next possibly stashed token.
260 *
261 * @return {Token}
262 * @api private
263 */
264
265 stashed: function() {
266 return this.stash.shift();
267 },
268
269 /**
270 * EOS | trailing outdents.
271 */
272
273 eos: function() {
274 if (this.str.length) return;
275 if (this.indentStack.length) {
276 this.indentStack.shift();
277 return new Token('outdent');
278 } else {
279 return new Token('eos');
280 }
281 },
282
283 /**
284 * url char
285 */
286
287 urlchars: function() {
288 var captures;
289 if (!this.isURL) return;
290 if (captures = /^[\/:@.;?&=*!,<>#%0-9]+/.exec(this.str)) {
291 this.skip(captures);
292 return new Token('literal', new nodes.Literal(captures[0]));
293 }
294 },
295
296 /**
297 * ';' [ \t]*
298 */
299
300 sep: function() {
301 var captures;
302 if (captures = /^;[ \t]*/.exec(this.str)) {
303 this.skip(captures);
304 return new Token(';');
305 }
306 },
307
308 /**
309 * '\r'
310 */
311
312 eol: function() {
313 if ('\r' == this.str[0]) {
314 ++this.lineno;
315 this.skip(1);
316 return this.advance();
317 }
318 },
319
320 /**
321 * ' '+
322 */
323
324 space: function() {
325 var captures;
326 if (captures = /^([ \t]+)/.exec(this.str)) {
327 this.skip(captures);
328 return new Token('space');
329 }
330 },
331
332 /**
333 * '\\' . ' '*
334 */
335
336 escaped: function() {
337 var captures;
338 if (captures = /^\\(.)[ \t]*/.exec(this.str)) {
339 var c = captures[1];
340 this.skip(captures);
341 return new Token('ident', new nodes.Literal(c));
342 }
343 },
344
345 /**
346 * '@css' ' '* '{' .* '}' ' '*
347 */
348
349 literal: function() {
350 // HACK attack !!!
351 var captures;
352 if (captures = /^@css[ \t]*\{/.exec(this.str)) {
353 this.skip(captures);
354 var c
355 , braces = 1
356 , css = ''
357 , node;
358 while (c = this.str[0]) {
359 this.str = this.str.substr(1);
360 switch (c) {
361 case '{': ++braces; break;
362 case '}': --braces; break;
363 case '\n': ++this.lineno; break;
364 }
365 css += c;
366 if (!braces) break;
367 }
368 css = css.replace(/\s*}$/, '');
369 node = new nodes.Literal(css);
370 node.css = true;
371 return new Token('literal', node);
372 }
373 },
374
375 /**
376 * '!important' ' '*
377 */
378
379 important: function() {
380 var captures;
381 if (captures = /^!important[ \t]*/.exec(this.str)) {
382 this.skip(captures);
383 return new Token('ident', new nodes.Literal('!important'));
384 }
385 },
386
387 /**
388 * '{' | '}'
389 */
390
391 brace: function() {
392 var captures;
393 if (captures = /^([{}])/.exec(this.str)) {
394 this.skip(1);
395 var brace = captures[1];
396 return new Token(brace, brace);
397 }
398 },
399
400 /**
401 * '(' | ')' ' '*
402 */
403
404 paren: function() {
405 var captures;
406 if (captures = /^([()])([ \t]*)/.exec(this.str)) {
407 var paren = captures[1];
408 this.skip(captures);
409 if (')' == paren) this.isURL = false;
410 var tok = new Token(paren, paren);
411 tok.space = captures[2];
412 return tok;
413 }
414 },
415
416 /**
417 * 'null'
418 */
419
420 null: function() {
421 var captures
422 , tok;
423 if (captures = /^(null)\b[ \t]*/.exec(this.str)) {
424 this.skip(captures);
425 if (this.isPartOfSelector()) {
426 tok = new Token('ident', new nodes.Ident(captures[0]));
427 } else {
428 tok = new Token('null', nodes.null);
429 }
430 return tok;
431 }
432 },
433
434 /**
435 * 'if'
436 * | 'else'
437 * | 'unless'
438 * | 'return'
439 * | 'for'
440 * | 'in'
441 */
442
443 keyword: function() {
444 var captures
445 , tok;
446 if (captures = /^(return|if|else|unless|for|in)\b[ \t]*/.exec(this.str)) {
447 var keyword = captures[1];
448 this.skip(captures);
449 if (this.isPartOfSelector()) {
450 tok = new Token('ident', new nodes.Ident(captures[0]));
451 } else {
452 tok = new Token(keyword, keyword);
453 }
454 return tok;
455 }
456 },
457
458 /**
459 * 'not'
460 * | 'and'
461 * | 'or'
462 * | 'is'
463 * | 'is not'
464 * | 'isnt'
465 * | 'is a'
466 * | 'is defined'
467 */
468
469 namedop: function() {
470 var captures
471 , tok;
472 if (captures = /^(not|and|or|is a|is defined|isnt|is not|is)(?!-)\b([ \t]*)/.exec(this.str)) {
473 var op = captures[1];
474 this.skip(captures);
475 if (this.isPartOfSelector()) {
476 tok = new Token('ident', new nodes.Ident(captures[0]));
477 } else {
478 op = alias[op] || op;
479 tok = new Token(op, op);
480 }
481 tok.space = captures[2];
482 return tok;
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
523 op: function() {
524 var captures;
525 if (captures = /^([.]{1,3}|&&|\|\||[!<>=?:]=|\*\*|[-+*\/%]=?|[,=?:!~<>&\[\]])([ \t]*)/.exec(this.str)) {
526 var op = captures[1];
527 this.skip(captures);
528 op = alias[op] || op;
529 var tok = new Token(op, op);
530 tok.space = captures[2];
531 this.isURL = false;
532 return tok;
533 }
534 },
535
536 /**
537 * '@('
538 */
539
540 anonFunc: function() {
541 var tok;
542 if ('@' == this.str[0] && '(' == this.str[1]) {
543 this.skip(2);
544 tok = new Token('function', new nodes.Ident('anonymous'));
545 tok.anonymous = true;
546 return tok;
547 }
548 },
549
550 /**
551 * '@' (-(\w+)-)?[a-zA-Z0-9-_]+
552 */
553
554 atrule: function() {
555 var captures;
556 if (captures = /^@(?:-(\w+)-)?([a-zA-Z0-9-_]+)[ \t]*/.exec(this.str)) {
557 this.skip(captures);
558 var vendor = captures[1]
559 , type = captures[2]
560 , tok;
561 switch (type) {
562 case 'require':
563 case 'import':
564 case 'charset':
565 case 'namespace':
566 case 'media':
567 case 'scope':
568 case 'supports':
569 return new Token(type);
570 case 'document':
571 return new Token('-moz-document');
572 case 'block':
573 return new Token('atblock');
574 case 'extend':
575 case 'extends':
576 return new Token('extend');
577 case 'keyframes':
578 return new Token(type, vendor);
579 default:
580 return new Token('atrule', (vendor ? '-' + vendor + '-' + type : type));
581 }
582 }
583 },
584
585 /**
586 * '//' *
587 */
588
589 comment: function() {
590 // Single line
591 if ('/' == this.str[0] && '/' == this.str[1]) {
592 var end = this.str.indexOf('\n');
593 if (-1 == end) end = this.str.length;
594 this.skip(end);
595 return this.advance();
596 }
597
598 // Multi-line
599 if ('/' == this.str[0] && '*' == this.str[1]) {
600 var end = this.str.indexOf('*/');
601 if (-1 == end) end = this.str.length;
602 var str = this.str.substr(0, end + 2)
603 , lines = str.split('\n').length - 1
604 , suppress = true
605 , inline = false;
606 this.lineno += lines;
607 this.skip(end + 2);
608 // output
609 if ('!' == str[2]) {
610 str = str.replace('*!', '*');
611 suppress = false;
612 }
613 if (this.prev && ';' == this.prev.type) inline = true;
614 return new Token('comment', new nodes.Comment(str, suppress, inline));
615 }
616 },
617
618 /**
619 * 'true' | 'false'
620 */
621
622 boolean: function() {
623 var captures;
624 if (captures = /^(true|false)\b([ \t]*)/.exec(this.str)) {
625 var val = nodes.Boolean('true' == captures[1]);
626 this.skip(captures);
627 var tok = new Token('boolean', val);
628 tok.space = captures[2];
629 return tok;
630 }
631 },
632
633 /**
634 * 'U+' [0-9A-Fa-f?]{1,6}(?:-[0-9A-Fa-f]{1,6})?
635 */
636
637 unicode: function() {
638 var captures;
639 if (captures = /^u\+[0-9a-f?]{1,6}(?:-[0-9a-f]{1,6})?/i.exec(this.str)) {
640 this.skip(captures);
641 return new Token('literal', new nodes.Literal(captures[0]));
642 }
643 },
644
645 /**
646 * -*[_a-zA-Z$] [-\w\d$]* '('
647 */
648
649 function: function() {
650 var captures;
651 if (captures = /^(-*[_a-zA-Z$][-\w\d$]*)\(([ \t]*)/.exec(this.str)) {
652 var name = captures[1];
653 this.skip(captures);
654 this.isURL = 'url' == name;
655 var tok = new Token('function', new nodes.Ident(name));
656 tok.space = captures[2];
657 return tok;
658 }
659 },
660
661 /**
662 * -*[_a-zA-Z$] [-\w\d$]*
663 */
664
665 ident: function() {
666 var captures;
667 if (captures = /^-*[_a-zA-Z$][-\w\d$]*/.exec(this.str)) {
668 this.skip(captures);
669 return new Token('ident', new nodes.Ident(captures[0]));
670 }
671 },
672
673 /**
674 * '\n' ' '+
675 */
676
677 newline: function() {
678 var captures, re;
679
680 // we have established the indentation regexp
681 if (this.indentRe){
682 captures = this.indentRe.exec(this.str);
683 // figure out if we are using tabs or spaces
684 } else {
685 // try tabs
686 re = /^\n([\t]*)[ \t]*/;
687 captures = re.exec(this.str);
688
689 // nope, try spaces
690 if (captures && !captures[1].length) {
691 re = /^\n([ \t]*)/;
692 captures = re.exec(this.str);
693 }
694
695 // established
696 if (captures && captures[1].length) this.indentRe = re;
697 }
698
699
700 if (captures) {
701 var tok
702 , indents = captures[1].length;
703
704 this.skip(captures);
705 if (this.str[0] === ' ' || this.str[0] === '\t') {
706 throw new errors.SyntaxError('Invalid indentation. You can use tabs or spaces to indent, but not both.');
707 }
708
709 // Blank line
710 if ('\n' == this.str[0]) return this.advance();
711
712 // Outdent
713 if (this.indentStack.length && indents < this.indentStack[0]) {
714 while (this.indentStack.length && this.indentStack[0] > indents) {
715 this.stash.push(new Token('outdent'));
716 this.indentStack.shift();
717 }
718 tok = this.stash.pop();
719 // Indent
720 } else if (indents && indents != this.indentStack[0]) {
721 this.indentStack.unshift(indents);
722 tok = new Token('indent');
723 // Newline
724 } else {
725 tok = new Token('newline');
726 }
727
728 return tok;
729 }
730 },
731
732 /**
733 * '-'? (digit+ | digit* '.' digit+) unit
734 */
735
736 unit: function() {
737 var captures;
738 if (captures = unit.exec(this.str)) {
739 this.skip(captures);
740 var n = parseFloat(captures[2]);
741 if ('-' == captures[1]) n = -n;
742 var node = new nodes.Unit(n, captures[3]);
743 node.raw = captures[0];
744 return new Token('unit', node);
745 }
746 },
747
748 /**
749 * '"' [^"]+ '"' | "'"" [^']+ "'"
750 */
751
752 string: function() {
753 var captures;
754 if (captures = /^("[^"]*"|'[^']*')[ \t]*/.exec(this.str)) {
755 var str = captures[1]
756 , quote = captures[0][0];
757 this.skip(captures);
758 str = str.slice(1,-1).replace(/\\n/g, '\n');
759 return new Token('string', new nodes.String(str, quote));
760 }
761 },
762
763 /**
764 * #rrggbbaa | #rrggbb | #rgba | #rgb | #nn | #n
765 */
766
767 color: function() {
768 return this.rrggbbaa()
769 || this.rrggbb()
770 || this.rgba()
771 || this.rgb()
772 || this.nn()
773 || this.n()
774 },
775
776 /**
777 * #n
778 */
779
780 n: function() {
781 var captures;
782 if (captures = /^#([a-fA-F0-9]{1})[ \t]*/.exec(this.str)) {
783 this.skip(captures);
784 var n = parseInt(captures[1] + captures[1], 16)
785 , color = new nodes.RGBA(n, n, n, 1);
786 color.raw = captures[0];
787 return new Token('color', color);
788 }
789 },
790
791 /**
792 * #nn
793 */
794
795 nn: function() {
796 var captures;
797 if (captures = /^#([a-fA-F0-9]{2})[ \t]*/.exec(this.str)) {
798 this.skip(captures);
799 var n = parseInt(captures[1], 16)
800 , color = new nodes.RGBA(n, n, n, 1);
801 color.raw = captures[0];
802 return new Token('color', color);
803 }
804 },
805
806 /**
807 * #rgb
808 */
809
810 rgb: function() {
811 var captures;
812 if (captures = /^#([a-fA-F0-9]{3})[ \t]*/.exec(this.str)) {
813 this.skip(captures);
814 var rgb = captures[1]
815 , r = parseInt(rgb[0] + rgb[0], 16)
816 , g = parseInt(rgb[1] + rgb[1], 16)
817 , b = parseInt(rgb[2] + rgb[2], 16)
818 , color = new nodes.RGBA(r, g, b, 1);
819 color.raw = captures[0];
820 return new Token('color', color);
821 }
822 },
823
824 /**
825 * #rgba
826 */
827
828 rgba: function() {
829 var captures;
830 if (captures = /^#([a-fA-F0-9]{4})[ \t]*/.exec(this.str)) {
831 this.skip(captures);
832 var rgb = captures[1]
833 , r = parseInt(rgb[0] + rgb[0], 16)
834 , g = parseInt(rgb[1] + rgb[1], 16)
835 , b = parseInt(rgb[2] + rgb[2], 16)
836 , a = parseInt(rgb[3] + rgb[3], 16)
837 , color = new nodes.RGBA(r, g, b, a/255);
838 color.raw = captures[0];
839 return new Token('color', color);
840 }
841 },
842
843 /**
844 * #rrggbb
845 */
846
847 rrggbb: function() {
848 var captures;
849 if (captures = /^#([a-fA-F0-9]{6})[ \t]*/.exec(this.str)) {
850 this.skip(captures);
851 var rgb = captures[1]
852 , r = parseInt(rgb.substr(0, 2), 16)
853 , g = parseInt(rgb.substr(2, 2), 16)
854 , b = parseInt(rgb.substr(4, 2), 16)
855 , color = new nodes.RGBA(r, g, b, 1);
856 color.raw = captures[0];
857 return new Token('color', color);
858 }
859 },
860
861 /**
862 * #rrggbbaa
863 */
864
865 rrggbbaa: function() {
866 var captures;
867 if (captures = /^#([a-fA-F0-9]{8})[ \t]*/.exec(this.str)) {
868 this.skip(captures);
869 var rgb = captures[1]
870 , r = parseInt(rgb.substr(0, 2), 16)
871 , g = parseInt(rgb.substr(2, 2), 16)
872 , b = parseInt(rgb.substr(4, 2), 16)
873 , a = parseInt(rgb.substr(6, 2), 16)
874 , color = new nodes.RGBA(r, g, b, a/255);
875 color.raw = captures[0];
876 return new Token('color', color);
877 }
878 },
879
880 /**
881 * [^\n,;]+
882 */
883
884 selector: function() {
885 var captures;
886 if (captures = /^.*?(?=\/\/(?![^\[]*\])|[,\n{])/.exec(this.str)) {
887 var selector = captures[0];
888 this.skip(captures);
889 return new Token('selector', selector);
890 }
891 }
892};