UNPKG

12.7 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
15/**
16 * Operator aliases.
17 */
18
19var alias = {
20 'and': '&&'
21 , 'or': '||'
22 , 'is': '=='
23 , 'isnt': '!='
24 , 'is not': '!='
25};
26
27/**
28 * Numeric strings used for exceptions.
29 */
30
31var numberString = [, 'one', 'two'];
32
33/**
34 * Units.
35 */
36
37var units = [
38 'em'
39 , 'ex'
40 , 'px'
41 , 'mm'
42 , 'cm'
43 , 'in'
44 , 'pt'
45 , 'pc'
46 , 'deg'
47 , 'rad'
48 , 'grad'
49 , 'ms'
50 , 's'
51 , 'Hz'
52 , 'kHz'
53 , '%'].join('|');
54
55/**
56 * Unit RegExp.
57 */
58
59var unit = new RegExp('^(-)?(\\d+\\.\\d+|\\d+|\\.\\d+)(' + units + ')? *');
60
61/**
62 * Initialize a new `Lexer` with the given `str` and `options`.
63 *
64 * @param {String} str
65 * @param {Object} options
66 * @api private
67 */
68
69var Lexer = module.exports = function Lexer(str, options) {
70 options = options || {};
71 this.str = str.replace(/\r\n?/g, '\n');
72 this.stash = [];
73 this.indentStack = [];
74 this.indentRe = null;
75 this.lineno = 0;
76};
77
78/**
79 * Lexer prototype.
80 */
81
82Lexer.prototype = {
83
84 /**
85 * Custom inspect.
86 */
87
88 inspect: function(){
89 var tok
90 , tmp = this.str
91 , buf = [];
92 while ('eos' != (tok = this.next).type) {
93 buf.push(tok.inspect());
94 }
95 this.str = tmp;
96 this.prevIndents = 0;
97 return buf.concat(tok.inspect()).join('\n');
98 },
99
100 /**
101 * Lookahead `n` tokens.
102 *
103 * @param {Number} n
104 * @return {Object}
105 * @api private
106 */
107
108 lookahead: function(n){
109 var fetch = n - this.stash.length;
110 while (fetch-- > 0) this.stash.push(this.advance);
111 return this.stash[--n];
112 },
113
114 /**
115 * Consume the given `len`.
116 *
117 * @param {Number|Array} len
118 * @api private
119 */
120
121 skip: function(len){
122 this.str = this.str.substr(Array.isArray(len)
123 ? len[0].length
124 : len);
125 },
126
127 /**
128 * Fetch next token including those stashed by peek.
129 *
130 * @return {Token}
131 * @api private
132 */
133
134 get next() {
135 var tok = this.stashed || this.advance;
136 switch (tok.type) {
137 case 'newline':
138 case 'selector':
139 case 'indent':
140 ++this.lineno;
141 }
142 tok.lineno = this.lineno;
143 return tok;
144 },
145
146 /**
147 * Fetch next token.
148 *
149 * @return {Token}
150 * @api private
151 */
152
153 get advance() {
154 return this.eos
155 || this.null
156 || this.sep
157 || this.keyword
158 || this.urlchars
159 || this.atrule
160 || this.media
161 || this.comment
162 || this.newline
163 || this.escaped
164 || this.important
165 || this.literal
166 || this.function
167 || this.brace
168 || this.paren
169 || this.color
170 || this.string
171 || this.unit
172 || this.namedop
173 || this.boolean
174 || this.ident
175 || this.op
176 || this.space
177 || this.selector;
178 },
179
180 /**
181 * Lookahead a single token.
182 *
183 * @return {Token}
184 * @api private
185 */
186
187 get peek() {
188 return this.lookahead(1);
189 },
190
191 /**
192 * Return the next possibly stashed token.
193 *
194 * @return {Token}
195 * @api private
196 */
197
198 get stashed() {
199 return this.stash.shift();
200 },
201
202 /**
203 * EOS | trailing outdents.
204 */
205
206 get eos() {
207 if (this.str.length) return;
208 if (this.indentStack.length) {
209 this.indentStack.shift();
210 return new Token('outdent');
211 } else {
212 return new Token('eos');
213 }
214 },
215
216 /**
217 * url char
218 */
219
220 get urlchars() {
221 var captures;
222 if (!this.isURL) return;
223 if (captures = /^[\/:@.;?&=*!,<>#%0-9]+/.exec(this.str)) {
224 this.skip(captures);
225 return new Token('literal', new nodes.Literal(captures[0]));
226 }
227 },
228
229 /**
230 * ';' ' '*
231 */
232
233 get sep() {
234 var captures;
235 if (captures = /^; */.exec(this.str)) {
236 this.skip(captures);
237 return new Token(';');
238 }
239 },
240
241 /**
242 * ' '+
243 */
244
245 get space() {
246 var captures;
247 if (captures = /^( +)/.exec(this.str)) {
248 this.skip(captures);
249 return new Token('space');
250 }
251 },
252
253 /**
254 * '\\' . ' '*
255 */
256
257 get escaped() {
258 var captures;
259 if (captures = /^\\(.) */.exec(this.str)) {
260 var c = captures[1];
261 this.skip(captures);
262 return new Token('ident', new nodes.Literal(c));
263 }
264 },
265
266 /**
267 * '@css' ' '* '{' .* '}' ' '*
268 */
269
270 get literal() {
271 // HACK attack !!!
272 var captures;
273 if (captures = /^@css *\{/.exec(this.str)) {
274 this.skip(captures);
275 var c
276 , braces = 1
277 , css = '';
278 while (c = this.str[0]) {
279 this.str = this.str.substr(1);
280 switch (c) {
281 case '{': ++braces; break;
282 case '}': --braces; break;
283 }
284 css += c;
285 if (!braces) break;
286 }
287 css = css.replace(/\s*}$/, '');
288 return new Token('literal', new nodes.Literal(css));
289 }
290 },
291
292 /**
293 * '!important' ' '*
294 */
295
296 get important() {
297 var captures;
298 if (captures = /^!important */.exec(this.str)) {
299 this.skip(captures);
300 return new Token('ident', new nodes.Literal('!important'));
301 }
302 },
303
304 /**
305 * '{' | '}'
306 */
307
308 get brace() {
309 var captures;
310 if (captures = /^([{}])/.exec(this.str)) {
311 this.skip(1);
312 var brace = captures[1];
313 return new Token(brace, brace);
314 }
315 },
316
317 /**
318 * '(' | ')' ' '*
319 */
320
321 get paren() {
322 var captures;
323 if (captures = /^([()]) */.exec(this.str)) {
324 var paren = captures[1];
325 this.skip(captures);
326 if (')' == paren) this.isURL = false;
327 return new Token(paren, paren);
328 }
329 },
330
331 /**
332 * 'null'
333 */
334
335 get null() {
336 var captures;
337 if (captures = /^(null)\b */.exec(this.str)) {
338 this.skip(captures);
339 return new Token('null', nodes.null);
340 }
341 },
342
343 /**
344 * 'if'
345 * | 'else'
346 * | 'unless'
347 * | 'return'
348 * | 'for'
349 * | 'in'
350 */
351
352 get keyword() {
353 var captures;
354 if (captures = /^(return|if|else|unless|for|in)\b */.exec(this.str)) {
355 var keyword = captures[1];
356 this.skip(captures);
357 return new Token(keyword, keyword);
358 }
359 },
360
361 /**
362 * 'not'
363 * | 'and'
364 * | 'or'
365 * | 'is'
366 * | 'is not'
367 * | 'isnt'
368 * | 'is a'
369 * | 'is defined'
370 */
371
372 get namedop() {
373 var captures;
374 if (captures = /^(not|and|or|is a|is defined|isnt|is not|is)\b( *)/.exec(this.str)) {
375 var op = captures[1];
376 this.skip(captures);
377 op = alias[op] || op;
378 var tok = new Token(op, op);
379 tok.space = captures[2];
380 return tok;
381 }
382 },
383
384 /**
385 * ','
386 * | '+'
387 * | '+='
388 * | '-'
389 * | '-='
390 * | '*'
391 * | '*='
392 * | '/'
393 * | '/='
394 * | '%'
395 * | '%='
396 * | '**'
397 * | '!'
398 * | '&'
399 * | '&&'
400 * | '||'
401 * | '>'
402 * | '>='
403 * | '<'
404 * | '<='
405 * | '='
406 * | '=='
407 * | '!='
408 * | '!'
409 * | '~'
410 * | '?='
411 * | '?'
412 * | ':'
413 * | '['
414 * | ']'
415 * | '..'
416 * | '...'
417 */
418
419 get op() {
420 var captures;
421 if (captures = /^([.]{2,3}|&&|\|\||[!<>=?]=|\*\*|[-+*\/%]=?|[,=?:!~<>&\[\]])( *)/.exec(this.str)) {
422 var op = captures[1];
423 this.skip(captures);
424 op = alias[op] || op;
425 var tok = new Token(op, op);
426 tok.space = captures[2];
427 return tok;
428 }
429 },
430
431 /**
432 * '@media' ([^{\n]+)
433 */
434
435 get media() {
436 var captures;
437 if (captures = /^@media *([^{\n]+)/.exec(this.str)) {
438 this.skip(captures);
439 return new Token('media', captures[1].trim());
440 }
441 },
442
443 /**
444 * '@' ('import' | 'keyframes' | 'charset' | 'page')
445 */
446
447 get atrule() {
448 var captures;
449 if (captures = /^@(import|keyframes|charset|page) */.exec(this.str)) {
450 this.skip(captures);
451 return new Token(captures[1]);
452 }
453 },
454
455 /**
456 * '//' *
457 */
458
459 get comment() {
460 // Single line
461 if ('/' == this.str[0] && '/' == this.str[1]) {
462 var end = this.str.indexOf('\n');
463 if (-1 == end) end = this.str.length;
464 this.skip(end);
465 return this.advance;
466 }
467
468 // Multi-line
469 if ('/' == this.str[0] && '*' == this.str[1]) {
470 var end = this.str.indexOf('*/');
471 if (-1 == end) end = this.str.length;
472 var str = this.str.substr(0, end + 2)
473 , lines = str.split('\n').length - 1;
474 this.lineno += lines;
475 this.skip(end + 2);
476 return this.allowComments
477 ? new Token('comment', str)
478 : this.advance;
479 }
480 },
481
482 /**
483 * 'true' | 'false'
484 */
485
486 get boolean() {
487 var captures;
488 if (captures = /^(true|false)\b( *)/.exec(this.str)) {
489 var val = 'true' == captures[1]
490 ? nodes.true
491 : nodes.false;
492 this.skip(captures);
493 var tok = new Token('boolean', val);
494 tok.space = captures[2];
495 return tok;
496 }
497 },
498
499 /**
500 * -?[a-zA-Z$] [-\w\d$]* '('
501 */
502
503 get function() {
504 var captures;
505 if (captures = /^(-?[a-zA-Z$][-\w\d$]*)\(( *)/.exec(this.str)) {
506 var name = captures[1];
507 this.skip(captures);
508 this.isURL = 'url' == name;
509 var tok = new Token('function', new nodes.Ident(name));
510 tok.space = captures[2];
511 return tok;
512 }
513 },
514
515 /**
516 * -?[a-zA-Z$] [-\w\d$]*
517 */
518
519 get ident() {
520 var captures;
521 if (captures = /^(-?[a-zA-Z$][-\w\d$]*)/.exec(this.str)) {
522 var name = captures[1];
523 this.skip(captures);
524 return new Token('ident', new nodes.Ident(name));
525 }
526 },
527
528 /**
529 * '\n' ' '+
530 */
531
532 get newline() {
533 var captures, re;
534
535 // we have established the indentation regexp
536 if (this.indentRe){
537 captures = this.indentRe.exec(this.str);
538 // figure out if we are using tabs or spaces
539 } else {
540 // try tabs
541 re = /^\n([\t]*) */;
542 captures = re.exec(this.str);
543
544 // nope, try spaces
545 if (captures && !captures[1].length) {
546 re = /^\n( *)/;
547 captures = re.exec(this.str);
548 }
549
550 // established
551 if (captures && captures[1].length) this.indentRe = re;
552 }
553
554
555 if (captures) {
556 var tok
557 , indents = captures[1].length;
558
559 this.skip(captures);
560 if (this.str[0] === ' ' || this.str[0] === '\t') {
561 throw new Error('Invalid indentation, you can use tabs or spaces to indent but not both');
562 }
563
564 // Reset state
565 this.isVariable = false;
566
567 // Blank line
568 if ('\n' == this.str[0]) {
569 ++this.lineno;
570 return this.advance;
571 }
572
573 // Outdent
574 if (this.indentStack.length && indents < this.indentStack[0]) {
575 while (this.indentStack.length && this.indentStack[0] > indents) {
576 this.stash.push(new Token('outdent'));
577 this.indentStack.shift();
578 }
579 tok = this.stash.pop();
580 // Indent
581 } else if (indents && indents != this.indentStack[0]) {
582 this.indentStack.unshift(indents);
583 tok = new Token('indent');
584 // Newline
585 } else {
586 tok = new Token('newline');
587 }
588
589 return tok;
590 }
591 },
592
593 /**
594 * '-'? (digit+ | digit* '.' digit+) unit
595 */
596
597 get unit() {
598 var captures;
599 if (captures = unit.exec(this.str)) {
600 this.skip(captures);
601 var n = parseFloat(captures[2]);
602 if ('-' == captures[1]) n = -n;
603 var node = new nodes.Unit(n, captures[3]);
604 return new Token('unit', node);
605 }
606 },
607
608 /**
609 * '"' [^"]+ '"' | "'"" [^']+ "'"
610 */
611
612 get string() {
613 var captures;
614 if (captures = /^("[^"]*"|'[^']*') */.exec(this.str)) {
615 var str = captures[1];
616 this.skip(captures);
617 return new Token('string', new nodes.String(str.slice(1,-1)));
618 }
619 },
620
621 /**
622 * #nnnnnn | #nnn
623 */
624
625 get color() {
626 return this.hex6 || this.hex3;
627 },
628
629 /**
630 * #nnn
631 */
632
633 get hex3() {
634 var captures;
635 if (captures = /^#([a-fA-F0-9]{3}) */.exec(this.str)) {
636 this.skip(captures);
637 var rgb = captures[1]
638 , r = parseInt(rgb[0] + rgb[0], 16)
639 , g = parseInt(rgb[1] + rgb[1], 16)
640 , b = parseInt(rgb[2] + rgb[2], 16)
641 , color = new nodes.Color(r, g, b, 1);
642 color.raw = captures[0];
643 return new Token('color', color);
644 }
645 },
646
647 /**
648 * #nnnnnn
649 */
650
651 get hex6() {
652 var captures;
653 if (captures = /^#([a-fA-F0-9]{6}) */.exec(this.str)) {
654 this.skip(captures);
655 var rgb = captures[1]
656 , r = parseInt(rgb.substr(0, 2), 16)
657 , g = parseInt(rgb.substr(2, 2), 16)
658 , b = parseInt(rgb.substr(4, 2), 16)
659 , color = new nodes.Color(r, g, b, 1);
660 color.raw = captures[0];
661 return new Token('color', color);
662 }
663 },
664
665 /**
666 * [^\n,;]+
667 */
668
669 get selector() {
670 var captures;
671 if (captures = /^[^{\n,]+/.exec(this.str)) {
672 var selector = captures[0];
673 this.skip(captures);
674 return new Token('selector', selector);
675 }
676 }
677};