diff options
Diffstat (limited to 'node_modules/stylus/lib/lexer.js')
| -rw-r--r-- | node_modules/stylus/lib/lexer.js | 882 |
1 files changed, 882 insertions, 0 deletions
diff --git a/node_modules/stylus/lib/lexer.js b/node_modules/stylus/lib/lexer.js new file mode 100644 index 00000000..a9457e50 --- /dev/null +++ b/node_modules/stylus/lib/lexer.js @@ -0,0 +1,882 @@ + +/*! + * 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 + '\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() + || this.eol() + || 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); + 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 = /^@(?:-(\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$][-\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); + } + } +}; |
