aboutsummaryrefslogtreecommitdiff
path: root/node_modules/jade/lib/lexer.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/jade/lib/lexer.js')
-rw-r--r--node_modules/jade/lib/lexer.js771
1 files changed, 771 insertions, 0 deletions
diff --git a/node_modules/jade/lib/lexer.js b/node_modules/jade/lib/lexer.js
new file mode 100644
index 000000000..bca314a9f
--- /dev/null
+++ b/node_modules/jade/lib/lexer.js
@@ -0,0 +1,771 @@
+
+/*!
+ * Jade - Lexer
+ * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
+ * 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();
+ }
+};