UNPKG

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