/*!
* Stylus - Lexer
* Copyright(c) 2010 LearnBoost <dev@learnboost.com>
* MIT Licensed
*/
/**
* Module dependencies.
*/
var Token = require('./token')
, nodes = require('./nodes')
, errors = require('./errors')
, units = require('./units');
/**
* Expose `Lexer`.
*/
exports = module.exports = Lexer;
/**
* Operator aliases.
*/
var alias = {
'and': '&&'
, 'or': '||'
, 'is': '=='
, 'isnt': '!='
, 'is not': '!='
, ':=': '?='
};
/**
* Units.
*/
units = units.join('|');
/**
* Unit RegExp.
*/
var unit = new RegExp('^(-)?(\\d+\\.\\d+|\\d+|\\.\\d+)(' + units + ')?[ \\t]*');
/**
* Initialize a new `Lexer` with the given `str` and `options`.
*
* @param {String} str
* @param {Object} options
* @api private
*/
function Lexer(str, options) {
options = options || {};
this.stash = [];
this.indentStack = [];
this.indentRe = null;
this.lineno = 1;
function comment(str, val, offset, s) {
var inComment = s.lastIndexOf('/*', offset) > s.lastIndexOf('*/', offset) || "//" == s.substr(0, 2);
return inComment
? str
: val;
};
this.str = str
.replace(/\s+$/, '\n')
.replace(/\r\n?/g, '\n')
.replace(/\\ *\n/g, ' ')
.replace(/([,:(]) *\n\s*/g, comment)
.replace(/\s*\n *([,)])/g, comment);
};
/**
* Lexer prototype.
*/
Lexer.prototype = {
/**
* Custom inspect.
*/
inspect: function(){
var tok
, tmp = this.str
, buf = [];
while ('eos' != (tok = this.next()).type) {
buf.push(tok.inspect());
}
this.str = tmp;
this.prevIndents = 0;
return buf.concat(tok.inspect()).join('\n');
},
/**
* Lookahead `n` tokens.
*
* @param {Number} n
* @return {Object}
* @api private
*/
lookahead: function(n){
var fetch = n - this.stash.length;
while (fetch-- > 0) this.stash.push(this.advance());
return this.stash[--n];
},
/**
* Consume the given `len`.
*
* @param {Number|Array} len
* @api private
*/
skip: function(len){
this.str = this.str.substr(Array.isArray(len)
? len[0].length
: len);
},
/**
* Fetch next token including those stashed by peek.
*
* @return {Token}
* @api private
*/
next: function() {
var tok = this.stashed() || this.advance();
switch (tok.type) {
case 'newline':
case 'indent':
++this.lineno;
break;
case 'outdent':
if ('outdent' != this.prev.type) ++this.lineno;
}
this.prev = tok;
tok.lineno = this.lineno;
return tok;
},
/**
* Fetch next token.
*
* @return {Token}
* @api private
*/
advance: function() {
return this.eos()
|| this.null()
|| this.sep()
|| this.keyword()
|| this.urlchars()
|| this.atrule()
|| this.scope()
|| this.extends()
|| this.media()
|| this.mozdocument()
|| this.comment()
|| this.newline()
|| this.escaped()
|| this.important()
|| this.literal()
|| this.function()
|| this.brace()
|| this.paren()
|| this.color()
|| this.string()
|| this.unit()
|| this.namedop()
|| this.boolean()
|| this.ident()
|| this.op()
|| this.space()
|| this.selector();
},
/**
* Lookahead a single token.
*
* @return {Token}
* @api private
*/
peek: function() {
return this.lookahead(1);
},
/**
* Return the next possibly stashed token.
*
* @return {Token}
* @api private
*/
stashed: function() {
return this.stash.shift();
},
/**
* EOS | trailing outdents.
*/
eos: function() {
if (this.str.length) return;
if (this.indentStack.length) {
this.indentStack.shift();
return new Token('outdent');
} else {
return new Token('eos');
}
},
/**
* url char
*/
urlchars: function() {
var captures;
if (!this.isURL) return;
if (captures = /^[\/:@.;?&=*!,<>#%0-9]+/.exec(this.str)) {
this.skip(captures);
return new Token('literal', new nodes.Literal(captures[0]));
}
},
/**
* ';' [ \t]*
*/
sep: function() {
var captures;
if (captures = /^;[ \t]*/.exec(this.str)) {
this.skip(captures);
return new Token(';');
}
},
/**
* ' '+
*/
space: function() {
var captures;
if (captures = /^([ \t]+)/.exec(this.str)) {
this.skip(captures);
return new Token('space');
}
},
/**
* '\\' . ' '*
*/
escaped: function() {
var captures;
if (captures = /^\\(.)[ \t]*/.exec(this.str)) {
var c = captures[1];
this.skip(captures);
return new Token('ident', new nodes.Literal(c));
}
},
/**
* '@css' ' '* '{' .* '}' ' '*
*/
literal: function() {
// HACK attack !!!
var captures;
if (captures = /^@css[ \t]*\{/.exec(this.str)) {
this.skip(captures);
var c
, braces = 1
, css = '';
while (c = this.str[0]) {
this.str = this.str.substr(1);
switch (c) {
case '{': ++braces; break;
case '}': --braces; break;
}
css += c;
if (!braces) break;
}
css = css.replace(/\s*}$/, '');
return new Token('literal', new nodes.Literal(css));
}
},
/**
* '!important' ' '*
*/
important: function() {
var captures;
if (captures = /^!important[ \t]*/.exec(this.str)) {
this.skip(captures);
return new Token('ident', new nodes.Literal('!important'));
}
},
/**
* '{' | '}'
*/
brace: function() {
var captures;
if (captures = /^([{}])/.exec(this.str)) {
this.skip(1);
var brace = captures[1];
return new Token(brace, brace);
}
},
/**
* '(' | ')' ' '*
*/
paren: function() {
var captures;
if (captures = /^([()])([ \t]*)/.exec(this.str)) {
var paren = captures[1];
this.skip(captures);
if (')' == paren) this.isURL = false;
var tok = new Token(paren, paren);
tok.space = captures[2];
return tok;
}
},
/**
* 'null'
*/
null: function() {
var captures;
if (captures = /^(null)\b[ \t]*/.exec(this.str)) {
this.skip(captures);
return new Token('null', nodes.null);
}
},
/**
* 'if'
* | 'else'
* | 'unless'
* | 'return'
* | 'for'
* | 'in'
*/
keyword: function() {
var captures;
if (captures = /^(return|if|else|unless|for|in)\b[ \t]*/.exec(this.str)) {
var keyword = captures[1];
this.skip(captures);
return new Token(keyword, keyword);
}
},
/**
* 'not'
* | 'and'
* | 'or'
* | 'is'
* | 'is not'
* | 'isnt'
* | 'is a'
* | 'is defined'
*/
namedop: function() {
var captures;
if (captures = /^(not|and|or|is a|is defined|isnt|is not|is)(?!-)\b([ \t]*)/.exec(this.str)) {
var op = captures[1];
this.skip(captures);
op = alias[op] || op;
var tok = new Token(op, op);
tok.space = captures[2];
return tok;
}
},
/**
* ','
* | '+'
* | '+='
* | '-'
* | '-='
* | '*'
* | '*='
* | '/'
* | '/='
* | '%'
* | '%='
* | '**'
* | '!'
* | '&'
* | '&&'
* | '||'
* | '>'
* | '>='
* | '<'
* | '<='
* | '='
* | '=='
* | '!='
* | '!'
* | '~'
* | '?='
* | ':='
* | '?'
* | ':'
* | '['
* | ']'
* | '..'
* | '...'
*/
op: function() {
var captures;
if (captures = /^([.]{2,3}|&&|\|\||[!<>=?:]=|\*\*|[-+*\/%]=?|[,=?:!~<>&\[\]])([ \t]*)/.exec(this.str)) {
var op = captures[1];
this.skip(captures);
op = alias[op] || op;
var tok = new Token(op, op);
tok.space = captures[2];
this.isURL = false;
return tok;
}
},
/**
* '@extends' ([^{\n]+)
*/
extends: function() {
var captures;
if (captures = /^@extends?[ \t]*([^\/{\n;]+)/.exec(this.str)) {
this.skip(captures);
return new Token('extend', captures[1].trim());
}
},
/**
* '@media' ([^{\n]+)
*/
media: function() {
var captures;
if (captures = /^@media[ \t]*([^\/{\n]+)/.exec(this.str)) {
this.skip(captures);
return new Token('media', captures[1].trim());
}
},
/**
* '@-moz-document' ([^{\n]+)
*/
mozdocument: function() {
var captures;
if (captures = /^@-moz-document[ \t]*([^\/{\n]+)/.exec(this.str)) {
this.skip(captures);
return new Token('-moz-document', captures[1].trim());
}
},
/**
* '@scope' ([^{\n]+)
*/
scope: function() {
var captures;
if (captures = /^@scope[ \t]*([^\/{\n]+)/.exec(this.str)) {
this.skip(captures);
return new Token('scope', captures[1].trim());
}
},
/**
* '@' ('import' | 'keyframes' | 'charset' | 'page' | 'font-face')
*/
atrule: function() {
var captures;
if (captures = /^@(import|(?:-(\w+)-)?keyframes|charset|font-face|page)[ \t]*/.exec(this.str)) {
this.skip(captures);
var vendor = captures[2]
, type = captures[1];
if (vendor) type = 'keyframes';
return new Token(type, vendor);
}
},
/**
* '//' *
*/
comment: function() {
// Single line
if ('/' == this.str[0] && '/' == this.str[1]) {
var end = this.str.indexOf('\n');
if (-1 == end) end = this.str.length;
this.skip(end);
return this.advance();
}
// Multi-line
if ('/' == this.str[0] && '*' == this.str[1]) {
var end = this.str.indexOf('*/');
if (-1 == end) end = this.str.length;
var str = this.str.substr(0, end + 2)
, lines = str.split('\n').length - 1
, suppress = true;
this.lineno += lines;
this.skip(end + 2);
// output
if ('!' == str[2]) {
str = str.replace('*!', '*');
suppress = false;
}
return new Token('comment', new nodes.Comment(str, suppress));
}
},
/**
* 'true' | 'false'
*/
boolean: function() {
var captures;
if (captures = /^(true|false)\b([ \t]*)/.exec(this.str)) {
var val = nodes.Boolean('true' == captures[1]);
this.skip(captures);
var tok = new Token('boolean', val);
tok.space = captures[2];
return tok;
}
},
/**
* -*[_a-zA-Z$] [-\w\d$]* '('
*/
function: function() {
var captures;
if (captures = /^(-*[_a-zA-Z$][-\w\d$]*)\(([ \t]*)/.exec(this.str)) {
var name = captures[1];
this.skip(captures);
this.isURL = 'url' == name;
var tok = new Token('function', new nodes.Ident(name));
tok.space = captures[2];
return tok;
}
},
/**
* -*[_a-zA-Z$] [-\w\d$]*
*/
ident: function() {
var captures;
if (captures = /^(@)?(-*[_a-zA-Z$][-\w\d$]*)/.exec(this.str)) {
var at = captures[1]
, name = captures[2]
, id = new nodes.Ident(name);
this.skip(captures);
id.property = !! at;
return new Token('ident', id);
}
},
/**
* '\n' ' '+
*/
newline: function() {
var captures, re;
// we have established the indentation regexp
if (this.indentRe){
captures = this.indentRe.exec(this.str);
// figure out if we are using tabs or spaces
} else {
// try tabs
re = /^\n([\t]*)[ \t]*/;
captures = re.exec(this.str);
// nope, try spaces
if (captures && !captures[1].length) {
re = /^\n([ \t]*)/;
captures = re.exec(this.str);
}
// established
if (captures && captures[1].length) this.indentRe = re;
}
if (captures) {
var tok
, indents = captures[1].length;
this.skip(captures);
if (this.str[0] === ' ' || this.str[0] === '\t') {
throw new errors.SyntaxError('Invalid indentation. You can use tabs or spaces to indent, but not both.');
}
// Reset state
this.isVariable = false;
// Blank line
if ('\n' == this.str[0]) {
++this.lineno;
return this.advance();
}
// Outdent
if (this.indentStack.length && indents < this.indentStack[0]) {
while (this.indentStack.length && this.indentStack[0] > indents) {
this.stash.push(new Token('outdent'));
this.indentStack.shift();
}
tok = this.stash.pop();
// Indent
} else if (indents && indents != this.indentStack[0]) {
this.indentStack.unshift(indents);
tok = new Token('indent');
// Newline
} else {
tok = new Token('newline');
}
return tok;
}
},
/**
* '-'? (digit+ | digit* '.' digit+) unit
*/
unit: function() {
var captures;
if (captures = unit.exec(this.str)) {
this.skip(captures);
var n = parseFloat(captures[2]);
if ('-' == captures[1]) n = -n;
var node = new nodes.Unit(n, captures[3]);
return new Token('unit', node);
}
},
/**
* '"' [^"]+ '"' | "'"" [^']+ "'"
*/
string: function() {
var captures;
if (captures = /^("[^"]*"|'[^']*')[ \t]*/.exec(this.str)) {
var str = captures[1]
, quote = captures[0][0];
this.skip(captures);
str = str.slice(1,-1).replace(/\\n/g, '\n');
return new Token('string', new nodes.String(str, quote));
}
},
/**
* #rrggbbaa | #rrggbb | #rgba | #rgb | #nn | #n
*/
color: function() {
return this.rrggbbaa()
|| this.rrggbb()
|| this.rgba()
|| this.rgb()
|| this.nn()
|| this.n()
},
/**
* #n
*/
n: function() {
var captures;
if (captures = /^#([a-fA-F0-9]{1})[ \t]*/.exec(this.str)) {
this.skip(captures);
var n = parseInt(captures[1] + captures[1], 16)
, color = new nodes.RGBA(n, n, n, 1);
color.raw = captures[0];
return new Token('color', color);
}
},
/**
* #nn
*/
nn: function() {
var captures;
if (captures = /^#([a-fA-F0-9]{2})[ \t]*/.exec(this.str)) {
this.skip(captures);
var n = parseInt(captures[1], 16)
, color = new nodes.RGBA(n, n, n, 1);
color.raw = captures[0];
return new Token('color', color);
}
},
/**
* #rgb
*/
rgb: function() {
var captures;
if (captures = /^#([a-fA-F0-9]{3})[ \t]*/.exec(this.str)) {
this.skip(captures);
var rgb = captures[1]
, r = parseInt(rgb[0] + rgb[0], 16)
, g = parseInt(rgb[1] + rgb[1], 16)
, b = parseInt(rgb[2] + rgb[2], 16)
, color = new nodes.RGBA(r, g, b, 1);
color.raw = captures[0];
return new Token('color', color);
}
},
/**
* #rgba
*/
rgba: function() {
var captures;
if (captures = /^#([a-fA-F0-9]{4})[ \t]*/.exec(this.str)) {
this.skip(captures);
var rgb = captures[1]
, r = parseInt(rgb[0] + rgb[0], 16)
, g = parseInt(rgb[1] + rgb[1], 16)
, b = parseInt(rgb[2] + rgb[2], 16)
, a = parseInt(rgb[3] + rgb[3], 16)
, color = new nodes.RGBA(r, g, b, a/255);
color.raw = captures[0];
return new Token('color', color);
}
},
/**
* #rrggbb
*/
rrggbb: function() {
var captures;
if (captures = /^#([a-fA-F0-9]{6})[ \t]*/.exec(this.str)) {
this.skip(captures);
var rgb = captures[1]
, r = parseInt(rgb.substr(0, 2), 16)
, g = parseInt(rgb.substr(2, 2), 16)
, b = parseInt(rgb.substr(4, 2), 16)
, color = new nodes.RGBA(r, g, b, 1);
color.raw = captures[0];
return new Token('color', color);
}
},
/**
* #rrggbbaa
*/
rrggbbaa: function() {
var captures;
if (captures = /^#([a-fA-F0-9]{8})[ \t]*/.exec(this.str)) {
this.skip(captures);
var rgb = captures[1]
, r = parseInt(rgb.substr(0, 2), 16)
, g = parseInt(rgb.substr(2, 2), 16)
, b = parseInt(rgb.substr(4, 2), 16)
, a = parseInt(rgb.substr(6, 2), 16)
, color = new nodes.RGBA(r, g, b, a/255);
color.raw = captures[0];
return new Token('color', color);
}
},
/**
* [^\n,;]+
*/
selector: function() {
var captures;
if (captures = /^[^{\n,]+/.exec(this.str)) {
var selector = captures[0];
this.skip(captures);
return new Token('selector', selector);
}
}
};