UNPKG

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