123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897 |
- /*!
- * Stylus - Lexer
- * Copyright (c) Automattic <developer.wordpress.com>
- * MIT Licensed
- */
- /**
- * Module dependencies.
- */
- var Token = require('./token')
- , nodes = require('./nodes')
- , errors = require('./errors');
- /**
- * Expose `Lexer`.
- */
- exports = module.exports = Lexer;
- /**
- * Operator aliases.
- */
- var alias = {
- 'and': '&&'
- , 'or': '||'
- , 'is': '=='
- , 'isnt': '!='
- , 'is not': '!='
- , ':=': '?='
- };
- /**
- * 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;
- this.column = 1;
- // HACK!
- function comment(str, val, offset, s) {
- var inComment = s.lastIndexOf('/*', offset) > s.lastIndexOf('*/', offset)
- , commentIdx = s.lastIndexOf('//', offset)
- , i = s.lastIndexOf('\n', offset)
- , double = 0
- , single = 0;
- if (~commentIdx && commentIdx > i) {
- while (i != offset) {
- if ("'" == s[i]) single ? single-- : single++;
- if ('"' == s[i]) double ? double-- : double++;
- if ('/' == s[i] && '/' == s[i + 1]) {
- inComment = !single && !double;
- break;
- }
- ++i;
- }
- }
- return inComment
- ? str
- : ((val === ',' && /^[,\t\n]+$/.test(str)) ? str.replace(/\n/, '\r') : val + '\r');
- };
- // Remove UTF-8 BOM.
- if ('\uFEFF' == str.charAt(0)) str = str.slice(1);
- this.str = str
- .replace(/\s+$/, '\n')
- .replace(/\r\n?/g, '\n')
- .replace(/\\ *\n/g, '\r')
- .replace(/([,(:](?!\/\/[^ ])) *(?:\/\/[^\n]*|\/\*.*?\*\/)?\n\s*/g, comment)
- .replace(/\s*\n[ \t]*([,)])/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;
- 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){
- var chunk = len[0];
- len = chunk ? chunk.length : len;
- this.str = this.str.substr(len);
- if (chunk) {
- this.move(chunk);
- } else {
- this.column += len;
- }
- },
- /**
- * Move current line and column position.
- *
- * @param {String} str
- * @api private
- */
- move: function(str){
- var lines = str.match(/\n/g)
- , idx = str.lastIndexOf('\n');
- if (lines) this.lineno += lines.length;
- this.column = ~idx
- ? str.length - idx
- : this.column + str.length;
- },
- /**
- * Fetch next token including those stashed by peek.
- *
- * @return {Token}
- * @api private
- */
- next: function() {
- var tok = this.stashed() || this.advance();
- this.prev = tok;
- return tok;
- },
- /**
- * Check if the current token is a part of selector.
- *
- * @return {Boolean}
- * @api private
- */
- isPartOfSelector: function() {
- var tok = this.stash[this.stash.length - 1] || this.prev;
- switch (tok && tok.type) {
- // #for
- case 'color':
- return 2 == tok.val.raw.length;
- // .or
- case '.':
- // [is]
- case '[':
- return true;
- }
- return false;
- },
- /**
- * Fetch next token.
- *
- * @return {Token}
- * @api private
- */
- advance: function() {
- var column = this.column
- , line = this.lineno
- , tok = this.eos()
- || this.null()
- || this.sep()
- || this.keyword()
- || this.urlchars()
- || this.comment()
- || this.newline()
- || this.escaped()
- || this.important()
- || this.literal()
- || this.anonFunc()
- || this.atrule()
- || this.function()
- || this.brace()
- || this.paren()
- || this.color()
- || this.string()
- || this.unit()
- || this.namedop()
- || this.boolean()
- || this.unicode()
- || this.ident()
- || this.op()
- || (function () {
- var token = this.eol();
- if (token) {
- column = token.column;
- line = token.lineno;
- }
- return token;
- }).call(this)
- || this.space()
- || this.selector();
- tok.lineno = line;
- tok.column = column;
- return tok;
- },
- /**
- * 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(';');
- }
- },
- /**
- * '\r'
- */
- eol: function() {
- if ('\r' == this.str[0]) {
- ++this.lineno;
- this.skip(1);
- this.column = 1;
- while(this.space());
- return this.advance();
- }
- },
- /**
- * ' '+
- */
- 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 = ''
- , node;
- while (c = this.str[0]) {
- this.str = this.str.substr(1);
- switch (c) {
- case '{': ++braces; break;
- case '}': --braces; break;
- case '\n':
- case '\r':
- ++this.lineno;
- break;
- }
- css += c;
- if (!braces) break;
- }
- css = css.replace(/\s*}$/, '');
- node = new nodes.Literal(css);
- node.css = true;
- return new Token('literal', node);
- }
- },
- /**
- * '!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
- , tok;
- if (captures = /^(null)\b[ \t]*/.exec(this.str)) {
- this.skip(captures);
- if (this.isPartOfSelector()) {
- tok = new Token('ident', new nodes.Ident(captures[0]));
- } else {
- tok = new Token('null', nodes.null);
- }
- return tok;
- }
- },
- /**
- * 'if'
- * | 'else'
- * | 'unless'
- * | 'return'
- * | 'for'
- * | 'in'
- */
- keyword: function() {
- var captures
- , tok;
- if (captures = /^(return|if|else|unless|for|in)\b(?!-)[ \t]*/.exec(this.str)) {
- var keyword = captures[1];
- this.skip(captures);
- if (this.isPartOfSelector()) {
- tok = new Token('ident', new nodes.Ident(captures[0]));
- } else {
- tok = new Token(keyword, keyword);
- }
- return tok;
- }
- },
- /**
- * 'not'
- * | 'and'
- * | 'or'
- * | 'is'
- * | 'is not'
- * | 'isnt'
- * | 'is a'
- * | 'is defined'
- */
- namedop: function() {
- var captures
- , tok;
- 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);
- if (this.isPartOfSelector()) {
- tok = new Token('ident', new nodes.Ident(captures[0]));
- } else {
- op = alias[op] || op;
- tok = new Token(op, op);
- }
- tok.space = captures[2];
- return tok;
- }
- },
- /**
- * ','
- * | '+'
- * | '+='
- * | '-'
- * | '-='
- * | '*'
- * | '*='
- * | '/'
- * | '/='
- * | '%'
- * | '%='
- * | '**'
- * | '!'
- * | '&'
- * | '&&'
- * | '||'
- * | '>'
- * | '>='
- * | '<'
- * | '<='
- * | '='
- * | '=='
- * | '!='
- * | '!'
- * | '~'
- * | '?='
- * | ':='
- * | '?'
- * | ':'
- * | '['
- * | ']'
- * | '.'
- * | '..'
- * | '...'
- */
- op: function() {
- var captures;
- if (captures = /^([.]{1,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;
- }
- },
- /**
- * '@('
- */
- anonFunc: function() {
- var tok;
- if ('@' == this.str[0] && '(' == this.str[1]) {
- this.skip(2);
- tok = new Token('function', new nodes.Ident('anonymous'));
- tok.anonymous = true;
- return tok;
- }
- },
- /**
- * '@' (-(\w+)-)?[a-zA-Z0-9-_]+
- */
- atrule: function() {
- var captures;
- if (captures = /^@(?!apply)(?:-(\w+)-)?([a-zA-Z0-9-_]+)[ \t]*/.exec(this.str)) {
- this.skip(captures);
- var vendor = captures[1]
- , type = captures[2]
- , tok;
- switch (type) {
- case 'require':
- case 'import':
- case 'charset':
- case 'namespace':
- case 'media':
- case 'scope':
- case 'supports':
- return new Token(type);
- case 'document':
- return new Token('-moz-document');
- case 'block':
- return new Token('atblock');
- case 'extend':
- case 'extends':
- return new Token('extend');
- case 'keyframes':
- return new Token(type, vendor);
- default:
- return new Token('atrule', (vendor ? '-' + vendor + '-' + type : type));
- }
- }
- },
- /**
- * '//' *
- */
- 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|\r/).length - 1
- , suppress = true
- , inline = false;
- this.lineno += lines;
- this.skip(end + 2);
- // output
- if ('!' == str[2]) {
- str = str.replace('*!', '*');
- suppress = false;
- }
- if (this.prev && ';' == this.prev.type) inline = true;
- return new Token('comment', new nodes.Comment(str, suppress, inline));
- }
- },
- /**
- * '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;
- }
- },
- /**
- * 'U+' [0-9A-Fa-f?]{1,6}(?:-[0-9A-Fa-f]{1,6})?
- */
- unicode: function() {
- var captures;
- if (captures = /^u\+[0-9a-f?]{1,6}(?:-[0-9a-f]{1,6})?/i.exec(this.str)) {
- this.skip(captures);
- return new Token('literal', new nodes.Literal(captures[0]));
- }
- },
- /**
- * -*[_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$]|@apply)[-\w\d$]*/.exec(this.str)) {
- this.skip(captures);
- return new Token('ident', new nodes.Ident(captures[0]));
- }
- },
- /**
- * '\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.');
- }
- // Blank line
- if ('\n' == this.str[0]) 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 = /^(-)?(\d+\.\d+|\d+|\.\d+)(%|[a-zA-Z]+)?[ \t]*/.exec(this.str)) {
- this.skip(captures);
- var n = parseFloat(captures[2]);
- if ('-' == captures[1]) n = -n;
- var node = new nodes.Unit(n, captures[3]);
- node.raw = captures[0];
- 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);
- }
- }
- };
|