/*! * Jade - Lexer * Copyright(c) 2010 TJ Holowaychuk * MIT Licensed */ /** * Initialize `Lexer` with the given `str`. * * Options: * * - `colons` allow colons for attr delimiters * * @param {String} str * @param {Object} options * @api private */ var Lexer = module.exports = function Lexer(str, options) { options = options || {}; this.input = str.replace(/\r\n|\r/g, '\n'); this.colons = options.colons; this.deferredTokens = []; this.lastIndents = 0; this.lineno = 1; this.stash = []; this.indentStack = []; this.indentRe = null; this.pipeless = false; }; /** * Lexer prototype. */ Lexer.prototype = { /** * Construct a token with the given `type` and `val`. * * @param {String} type * @param {String} val * @return {Object} * @api private */ tok: function(type, val){ return { type: type , line: this.lineno , val: val } }, /** * Consume the given `len` of input. * * @param {Number} len * @api private */ consume: function(len){ this.input = this.input.substr(len); }, /** * Scan for `type` with the given `regexp`. * * @param {String} type * @param {RegExp} regexp * @return {Object} * @api private */ scan: function(regexp, type){ var captures; if (captures = regexp.exec(this.input)) { this.consume(captures[0].length); return this.tok(type, captures[1]); } }, /** * Defer the given `tok`. * * @param {Object} tok * @api private */ defer: function(tok){ this.deferredTokens.push(tok); }, /** * 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.next()); return this.stash[--n]; }, /** * Return the indexOf `start` / `end` delimiters. * * @param {String} start * @param {String} end * @return {Number} * @api private */ indexOfDelimiters: function(start, end){ var str = this.input , nstart = 0 , nend = 0 , pos = 0; for (var i = 0, len = str.length; i < len; ++i) { if (start == str.charAt(i)) { ++nstart; } else if (end == str.charAt(i)) { if (++nend == nstart) { pos = i; break; } } } return pos; }, /** * Stashed token. */ stashed: function() { return this.stash.length && this.stash.shift(); }, /** * Deferred token. */ deferred: function() { return this.deferredTokens.length && this.deferredTokens.shift(); }, /** * end-of-source. */ eos: function() { if (this.input.length) return; if (this.indentStack.length) { this.indentStack.shift(); return this.tok('outdent'); } else { return this.tok('eos'); } }, /** * Blank line. */ blank: function() { var captures; if (captures = /^\n *\n/.exec(this.input)) { this.consume(captures[0].length - 1); if (this.pipeless) return this.tok('text', ''); return this.next(); } }, /** * Comment. */ comment: function() { var captures; if (captures = /^ *\/\/(-)?([^\n]*)/.exec(this.input)) { this.consume(captures[0].length); var tok = this.tok('comment', captures[2]); tok.buffer = '-' != captures[1]; return tok; } }, /** * Interpolated tag. */ interpolation: function() { var captures; if (captures = /^#\{(.*?)\}/.exec(this.input)) { this.consume(captures[0].length); return this.tok('interpolation', captures[1]); } }, /** * Tag. */ tag: function() { var captures; if (captures = /^(\w[-:\w]*)(\/?)/.exec(this.input)) { this.consume(captures[0].length); var tok, name = captures[1]; if (':' == name[name.length - 1]) { name = name.slice(0, -1); tok = this.tok('tag', name); this.defer(this.tok(':')); while (' ' == this.input[0]) this.input = this.input.substr(1); } else { tok = this.tok('tag', name); } tok.selfClosing = !! captures[2]; return tok; } }, /** * Filter. */ filter: function() { return this.scan(/^:(\w+)/, 'filter'); }, /** * Doctype. */ doctype: function() { return this.scan(/^(?:!!!|doctype) *([^\n]+)?/, 'doctype'); }, /** * Id. */ id: function() { return this.scan(/^#([\w-]+)/, 'id'); }, /** * Class. */ className: function() { return this.scan(/^\.([\w-]+)/, 'class'); }, /** * Text. */ text: function() { return this.scan(/^(?:\| ?| ?)?([^\n]+)/, 'text'); }, /** * Extends. */ "extends": function() { return this.scan(/^extends? +([^\n]+)/, 'extends'); }, /** * Block prepend. */ prepend: function() { var captures; if (captures = /^prepend +([^\n]+)/.exec(this.input)) { this.consume(captures[0].length); var mode = 'prepend' , name = captures[1] , tok = this.tok('block', name); tok.mode = mode; return tok; } }, /** * Block append. */ append: function() { var captures; if (captures = /^append +([^\n]+)/.exec(this.input)) { this.consume(captures[0].length); var mode = 'append' , name = captures[1] , tok = this.tok('block', name); tok.mode = mode; return tok; } }, /** * Block. */ block: function() { var captures; if (captures = /^block\b *(?:(prepend|append) +)?([^\n]*)/.exec(this.input)) { this.consume(captures[0].length); var mode = captures[1] || 'replace' , name = captures[2] , tok = this.tok('block', name); tok.mode = mode; return tok; } }, /** * Yield. */ yield: function() { return this.scan(/^yield */, 'yield'); }, /** * Include. */ include: function() { return this.scan(/^include +([^\n]+)/, 'include'); }, /** * Case. */ "case": function() { return this.scan(/^case +([^\n]+)/, 'case'); }, /** * When. */ when: function() { return this.scan(/^when +([^:\n]+)/, 'when'); }, /** * Default. */ "default": function() { return this.scan(/^default */, 'default'); }, /** * Assignment. */ assignment: function() { var captures; if (captures = /^(\w+) += *([^;\n]+)( *;? *)/.exec(this.input)) { this.consume(captures[0].length); var name = captures[1] , val = captures[2]; return this.tok('code', 'var ' + name + ' = (' + val + ');'); } }, /** * Call mixin. */ call: function(){ var captures; if (captures = /^\+([-\w]+)/.exec(this.input)) { this.consume(captures[0].length); var tok = this.tok('call', captures[1]); // Check for args (not attributes) if (captures = /^ *\((.*?)\)/.exec(this.input)) { if (!/^ *[-\w]+ *=/.test(captures[1])) { this.consume(captures[0].length); tok.args = captures[1]; } } return tok; } }, /** * Mixin. */ mixin: function(){ var captures; if (captures = /^mixin +([-\w]+)(?: *\((.*)\))?/.exec(this.input)) { this.consume(captures[0].length); var tok = this.tok('mixin', captures[1]); tok.args = captures[2]; return tok; } }, /** * Conditional. */ conditional: function() { var captures; if (captures = /^(if|unless|else if|else)\b([^\n]*)/.exec(this.input)) { this.consume(captures[0].length); var type = captures[1] , js = captures[2]; switch (type) { case 'if': js = 'if (' + js + ')'; break; case 'unless': js = 'if (!(' + js + '))'; break; case 'else if': js = 'else if (' + js + ')'; break; case 'else': js = 'else'; break; } return this.tok('code', js); } }, /** * While. */ "while": function() { var captures; if (captures = /^while +([^\n]+)/.exec(this.input)) { this.consume(captures[0].length); return this.tok('code', 'while (' + captures[1] + ')'); } }, /** * Each. */ each: function() { var captures; if (captures = /^(?:- *)?(?:each|for) +(\w+)(?: *, *(\w+))? * in *([^\n]+)/.exec(this.input)) { this.consume(captures[0].length); var tok = this.tok('each', captures[1]); tok.key = captures[2] || '$index'; tok.code = captures[3]; return tok; } }, /** * Code. */ code: function() { var captures; if (captures = /^(!?=|-)([^\n]+)/.exec(this.input)) { this.consume(captures[0].length); var flags = captures[1]; captures[1] = captures[2]; var tok = this.tok('code', captures[1]); tok.escape = flags[0] === '='; tok.buffer = flags[0] === '=' || flags[1] === '='; return tok; } }, /** * Attributes. */ attrs: function() { if ('(' == this.input.charAt(0)) { var index = this.indexOfDelimiters('(', ')') , str = this.input.substr(1, index-1) , tok = this.tok('attrs') , len = str.length , colons = this.colons , states = ['key'] , escapedAttr , key = '' , val = '' , quote , c , p; function state(){ return states[states.length - 1]; } function interpolate(attr) { return attr.replace(/#\{([^}]+)\}/g, function(_, expr){ return quote + " + (" + expr + ") + " + quote; }); } this.consume(index + 1); tok.attrs = {}; tok.escaped = {}; function parse(c) { var real = c; // TODO: remove when people fix ":" if (colons && ':' == c) c = '='; switch (c) { case ',': case '\n': switch (state()) { case 'expr': case 'array': case 'string': case 'object': val += c; break; default: states.push('key'); val = val.trim(); key = key.trim(); if ('' == key) return; key = key.replace(/^['"]|['"]$/g, '').replace('!', ''); tok.escaped[key] = escapedAttr; tok.attrs[key] = '' == val ? true : interpolate(val); key = val = ''; } break; case '=': switch (state()) { case 'key char': key += real; break; case 'val': case 'expr': case 'array': case 'string': case 'object': val += real; break; default: escapedAttr = '!' != p; states.push('val'); } break; case '(': if ('val' == state() || 'expr' == state()) states.push('expr'); val += c; break; case ')': if ('expr' == state() || 'val' == state()) states.pop(); val += c; break; case '{': if ('val' == state()) states.push('object'); val += c; break; case '}': if ('object' == state()) states.pop(); val += c; break; case '[': if ('val' == state()) states.push('array'); val += c; break; case ']': if ('array' == state()) states.pop(); val += c; break; case '"': case "'": switch (state()) { case 'key': states.push('key char'); break; case 'key char': states.pop(); break; case 'string': if (c == quote) states.pop(); val += c; break; default: states.push('string'); val += c; quote = c; } break; case '': break; default: switch (state()) { case 'key': case 'key char': key += c; break; default: val += c; } } p = c; } for (var i = 0; i < len; ++i) { parse(str.charAt(i)); } parse(','); if ('/' == this.input.charAt(0)) { this.consume(1); tok.selfClosing = true; } return tok; } }, /** * Indent | Outdent | Newline. */ indent: function() { var captures, re; // established regexp if (this.indentRe) { captures = this.indentRe.exec(this.input); // determine regexp } else { // tabs re = /^\n(\t*) */; captures = re.exec(this.input); // spaces if (captures && !captures[1].length) { re = /^\n( *)/; captures = re.exec(this.input); } // established if (captures && captures[1].length) this.indentRe = re; } if (captures) { var tok , indents = captures[1].length; ++this.lineno; this.consume(indents + 1); if (' ' == this.input[0] || '\t' == this.input[0]) { throw new Error('Invalid indentation, you can use tabs or spaces but not both'); } // blank line if ('\n' == this.input[0]) return this.tok('newline'); // outdent if (this.indentStack.length && indents < this.indentStack[0]) { while (this.indentStack.length && this.indentStack[0] > indents) { this.stash.push(this.tok('outdent')); this.indentStack.shift(); } tok = this.stash.pop(); // indent } else if (indents && indents != this.indentStack[0]) { this.indentStack.unshift(indents); tok = this.tok('indent', indents); // newline } else { tok = this.tok('newline'); } return tok; } }, /** * Pipe-less text consumed only when * pipeless is true; */ pipelessText: function() { if (this.pipeless) { if ('\n' == this.input[0]) return; var i = this.input.indexOf('\n'); if (-1 == i) i = this.input.length; var str = this.input.substr(0, i); this.consume(str.length); return this.tok('text', str); } }, /** * ':' */ colon: function() { return this.scan(/^: */, ':'); }, /** * Return the next token object, or those * previously stashed by lookahead. * * @return {Object} * @api private */ advance: function(){ return this.stashed() || this.next(); }, /** * Return the next token object. * * @return {Object} * @api private */ next: function() { return this.deferred() || this.blank() || this.eos() || this.pipelessText() || this.yield() || this.doctype() || this.interpolation() || this["case"]() || this.when() || this["default"]() || this["extends"]() || this.append() || this.prepend() || this.block() || this.include() || this.mixin() || this.call() || this.conditional() || this.each() || this["while"]() || this.assignment() || this.tag() || this.filter() || this.code() || this.id() || this.className() || this.attrs() || this.indent() || this.comment() || this.colon() || this.text(); } };