aboutsummaryrefslogtreecommitdiff
path: root/node_modules/jade/lib/parser.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/jade/lib/parser.js')
-rw-r--r--node_modules/jade/lib/parser.js710
1 files changed, 710 insertions, 0 deletions
diff --git a/node_modules/jade/lib/parser.js b/node_modules/jade/lib/parser.js
new file mode 100644
index 000000000..92f2af0cd
--- /dev/null
+++ b/node_modules/jade/lib/parser.js
@@ -0,0 +1,710 @@
+
+/*!
+ * Jade - Parser
+ * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
+ * MIT Licensed
+ */
+
+/**
+ * Module dependencies.
+ */
+
+var Lexer = require('./lexer')
+ , nodes = require('./nodes');
+
+/**
+ * Initialize `Parser` with the given input `str` and `filename`.
+ *
+ * @param {String} str
+ * @param {String} filename
+ * @param {Object} options
+ * @api public
+ */
+
+var Parser = exports = module.exports = function Parser(str, filename, options){
+ this.input = str;
+ this.lexer = new Lexer(str, options);
+ this.filename = filename;
+ this.blocks = {};
+ this.mixins = {};
+ this.options = options;
+ this.contexts = [this];
+};
+
+/**
+ * Tags that may not contain tags.
+ */
+
+var textOnly = exports.textOnly = ['script', 'style'];
+
+/**
+ * Parser prototype.
+ */
+
+Parser.prototype = {
+
+ /**
+ * Push `parser` onto the context stack,
+ * or pop and return a `Parser`.
+ */
+
+ context: function(parser){
+ if (parser) {
+ this.contexts.push(parser);
+ } else {
+ return this.contexts.pop();
+ }
+ },
+
+ /**
+ * Return the next token object.
+ *
+ * @return {Object}
+ * @api private
+ */
+
+ advance: function(){
+ return this.lexer.advance();
+ },
+
+ /**
+ * Skip `n` tokens.
+ *
+ * @param {Number} n
+ * @api private
+ */
+
+ skip: function(n){
+ while (n--) this.advance();
+ },
+
+ /**
+ * Single token lookahead.
+ *
+ * @return {Object}
+ * @api private
+ */
+
+ peek: function() {
+ return this.lookahead(1);
+ },
+
+ /**
+ * Return lexer lineno.
+ *
+ * @return {Number}
+ * @api private
+ */
+
+ line: function() {
+ return this.lexer.lineno;
+ },
+
+ /**
+ * `n` token lookahead.
+ *
+ * @param {Number} n
+ * @return {Object}
+ * @api private
+ */
+
+ lookahead: function(n){
+ return this.lexer.lookahead(n);
+ },
+
+ /**
+ * Parse input returning a string of js for evaluation.
+ *
+ * @return {String}
+ * @api public
+ */
+
+ parse: function(){
+ var block = new nodes.Block, parser;
+ block.line = this.line();
+
+ while ('eos' != this.peek().type) {
+ if ('newline' == this.peek().type) {
+ this.advance();
+ } else {
+ block.push(this.parseExpr());
+ }
+ }
+
+ if (parser = this.extending) {
+ this.context(parser);
+ var ast = parser.parse();
+ this.context();
+ // hoist mixins
+ for (var name in this.mixins)
+ ast.unshift(this.mixins[name]);
+ return ast;
+ }
+
+ return block;
+ },
+
+ /**
+ * Expect the given type, or throw an exception.
+ *
+ * @param {String} type
+ * @api private
+ */
+
+ expect: function(type){
+ if (this.peek().type === type) {
+ return this.advance();
+ } else {
+ throw new Error('expected "' + type + '", but got "' + this.peek().type + '"');
+ }
+ },
+
+ /**
+ * Accept the given `type`.
+ *
+ * @param {String} type
+ * @api private
+ */
+
+ accept: function(type){
+ if (this.peek().type === type) {
+ return this.advance();
+ }
+ },
+
+ /**
+ * tag
+ * | doctype
+ * | mixin
+ * | include
+ * | filter
+ * | comment
+ * | text
+ * | each
+ * | code
+ * | yield
+ * | id
+ * | class
+ * | interpolation
+ */
+
+ parseExpr: function(){
+ switch (this.peek().type) {
+ case 'tag':
+ return this.parseTag();
+ case 'mixin':
+ return this.parseMixin();
+ case 'block':
+ return this.parseBlock();
+ case 'case':
+ return this.parseCase();
+ case 'when':
+ return this.parseWhen();
+ case 'default':
+ return this.parseDefault();
+ case 'extends':
+ return this.parseExtends();
+ case 'include':
+ return this.parseInclude();
+ case 'doctype':
+ return this.parseDoctype();
+ case 'filter':
+ return this.parseFilter();
+ case 'comment':
+ return this.parseComment();
+ case 'text':
+ return this.parseText();
+ case 'each':
+ return this.parseEach();
+ case 'code':
+ return this.parseCode();
+ case 'call':
+ return this.parseCall();
+ case 'interpolation':
+ return this.parseInterpolation();
+ case 'yield':
+ this.advance();
+ var block = new nodes.Block;
+ block.yield = true;
+ return block;
+ case 'id':
+ case 'class':
+ var tok = this.advance();
+ this.lexer.defer(this.lexer.tok('tag', 'div'));
+ this.lexer.defer(tok);
+ return this.parseExpr();
+ default:
+ throw new Error('unexpected token "' + this.peek().type + '"');
+ }
+ },
+
+ /**
+ * Text
+ */
+
+ parseText: function(){
+ var tok = this.expect('text')
+ , node = new nodes.Text(tok.val);
+ node.line = this.line();
+ return node;
+ },
+
+ /**
+ * ':' expr
+ * | block
+ */
+
+ parseBlockExpansion: function(){
+ if (':' == this.peek().type) {
+ this.advance();
+ return new nodes.Block(this.parseExpr());
+ } else {
+ return this.block();
+ }
+ },
+
+ /**
+ * case
+ */
+
+ parseCase: function(){
+ var val = this.expect('case').val
+ , node = new nodes.Case(val);
+ node.line = this.line();
+ node.block = this.block();
+ return node;
+ },
+
+ /**
+ * when
+ */
+
+ parseWhen: function(){
+ var val = this.expect('when').val
+ return new nodes.Case.When(val, this.parseBlockExpansion());
+ },
+
+ /**
+ * default
+ */
+
+ parseDefault: function(){
+ this.expect('default');
+ return new nodes.Case.When('default', this.parseBlockExpansion());
+ },
+
+ /**
+ * code
+ */
+
+ parseCode: function(){
+ var tok = this.expect('code')
+ , node = new nodes.Code(tok.val, tok.buffer, tok.escape)
+ , block
+ , i = 1;
+ node.line = this.line();
+ while (this.lookahead(i) && 'newline' == this.lookahead(i).type) ++i;
+ block = 'indent' == this.lookahead(i).type;
+ if (block) {
+ this.skip(i-1);
+ node.block = this.block();
+ }
+ return node;
+ },
+
+ /**
+ * comment
+ */
+
+ parseComment: function(){
+ var tok = this.expect('comment')
+ , node;
+
+ if ('indent' == this.peek().type) {
+ node = new nodes.BlockComment(tok.val, this.block(), tok.buffer);
+ } else {
+ node = new nodes.Comment(tok.val, tok.buffer);
+ }
+
+ node.line = this.line();
+ return node;
+ },
+
+ /**
+ * doctype
+ */
+
+ parseDoctype: function(){
+ var tok = this.expect('doctype')
+ , node = new nodes.Doctype(tok.val);
+ node.line = this.line();
+ return node;
+ },
+
+ /**
+ * filter attrs? text-block
+ */
+
+ parseFilter: function(){
+ var block
+ , tok = this.expect('filter')
+ , attrs = this.accept('attrs');
+
+ this.lexer.pipeless = true;
+ block = this.parseTextBlock();
+ this.lexer.pipeless = false;
+
+ var node = new nodes.Filter(tok.val, block, attrs && attrs.attrs);
+ node.line = this.line();
+ return node;
+ },
+
+ /**
+ * tag ':' attrs? block
+ */
+
+ parseASTFilter: function(){
+ var block
+ , tok = this.expect('tag')
+ , attrs = this.accept('attrs');
+
+ this.expect(':');
+ block = this.block();
+
+ var node = new nodes.Filter(tok.val, block, attrs && attrs.attrs);
+ node.line = this.line();
+ return node;
+ },
+
+ /**
+ * each block
+ */
+
+ parseEach: function(){
+ var tok = this.expect('each')
+ , node = new nodes.Each(tok.code, tok.val, tok.key);
+ node.line = this.line();
+ node.block = this.block();
+ return node;
+ },
+
+ /**
+ * 'extends' name
+ */
+
+ parseExtends: function(){
+ var path = require('path')
+ , fs = require('fs')
+ , dirname = path.dirname
+ , basename = path.basename
+ , join = path.join;
+
+ if (!this.filename)
+ throw new Error('the "filename" option is required to extend templates');
+
+ var path = this.expect('extends').val.trim()
+ , dir = dirname(this.filename);
+
+ var path = join(dir, path + '.jade')
+ , str = fs.readFileSync(path, 'utf8')
+ , parser = new Parser(str, path, this.options);
+
+ parser.blocks = this.blocks;
+ parser.contexts = this.contexts;
+ this.extending = parser;
+
+ // TODO: null node
+ return new nodes.Literal('');
+ },
+
+ /**
+ * 'block' name block
+ */
+
+ parseBlock: function(){
+ var block = this.expect('block')
+ , mode = block.mode
+ , name = block.val.trim();
+
+ block = 'indent' == this.peek().type
+ ? this.block()
+ : new nodes.Block(new nodes.Literal(''));
+
+ var prev = this.blocks[name];
+
+ if (prev) {
+ switch (prev.mode) {
+ case 'append':
+ block.nodes = block.nodes.concat(prev.nodes);
+ prev = block;
+ break;
+ case 'prepend':
+ block.nodes = prev.nodes.concat(block.nodes);
+ prev = block;
+ break;
+ }
+ }
+
+ block.mode = mode;
+ return this.blocks[name] = prev || block;
+ },
+
+ /**
+ * include block?
+ */
+
+ parseInclude: function(){
+ var path = require('path')
+ , fs = require('fs')
+ , dirname = path.dirname
+ , basename = path.basename
+ , join = path.join;
+
+ var path = this.expect('include').val.trim()
+ , dir = dirname(this.filename);
+
+ if (!this.filename)
+ throw new Error('the "filename" option is required to use includes');
+
+ // no extension
+ if (!~basename(path).indexOf('.')) {
+ path += '.jade';
+ }
+
+ // non-jade
+ if ('.jade' != path.substr(-5)) {
+ var path = join(dir, path)
+ , str = fs.readFileSync(path, 'utf8');
+ return new nodes.Literal(str);
+ }
+
+ var path = join(dir, path)
+ , str = fs.readFileSync(path, 'utf8')
+ , parser = new Parser(str, path, this.options);
+ parser.blocks = this.blocks;
+ parser.mixins = this.mixins;
+
+ this.context(parser);
+ var ast = parser.parse();
+ this.context();
+ ast.filename = path;
+
+ if ('indent' == this.peek().type) {
+ ast.includeBlock().push(this.block());
+ }
+
+ return ast;
+ },
+
+ /**
+ * call ident block
+ */
+
+ parseCall: function(){
+ var tok = this.expect('call')
+ , name = tok.val
+ , args = tok.args
+ , mixin = new nodes.Mixin(name, args, new nodes.Block, true);
+
+ this.tag(mixin);
+ if (mixin.block.isEmpty()) mixin.block = null;
+ return mixin;
+ },
+
+ /**
+ * mixin block
+ */
+
+ parseMixin: function(){
+ var tok = this.expect('mixin')
+ , name = tok.val
+ , args = tok.args
+ , mixin;
+
+ // definition
+ if ('indent' == this.peek().type) {
+ mixin = new nodes.Mixin(name, args, this.block(), false);
+ this.mixins[name] = mixin;
+ return mixin;
+ // call
+ } else {
+ return new nodes.Mixin(name, args, null, true);
+ }
+ },
+
+ /**
+ * indent (text | newline)* outdent
+ */
+
+ parseTextBlock: function(){
+ var block = new nodes.Block;
+ block.line = this.line();
+ var spaces = this.expect('indent').val;
+ if (null == this._spaces) this._spaces = spaces;
+ var indent = Array(spaces - this._spaces + 1).join(' ');
+ while ('outdent' != this.peek().type) {
+ switch (this.peek().type) {
+ case 'newline':
+ this.advance();
+ break;
+ case 'indent':
+ this.parseTextBlock().nodes.forEach(function(node){
+ block.push(node);
+ });
+ break;
+ default:
+ var text = new nodes.Text(indent + this.advance().val);
+ text.line = this.line();
+ block.push(text);
+ }
+ }
+
+ if (spaces == this._spaces) this._spaces = null;
+ this.expect('outdent');
+ return block;
+ },
+
+ /**
+ * indent expr* outdent
+ */
+
+ block: function(){
+ var block = new nodes.Block;
+ block.line = this.line();
+ this.expect('indent');
+ while ('outdent' != this.peek().type) {
+ if ('newline' == this.peek().type) {
+ this.advance();
+ } else {
+ block.push(this.parseExpr());
+ }
+ }
+ this.expect('outdent');
+ return block;
+ },
+
+ /**
+ * interpolation (attrs | class | id)* (text | code | ':')? newline* block?
+ */
+
+ parseInterpolation: function(){
+ var tok = this.advance();
+ var tag = new nodes.Tag(tok.val);
+ tag.buffer = true;
+ return this.tag(tag);
+ },
+
+ /**
+ * tag (attrs | class | id)* (text | code | ':')? newline* block?
+ */
+
+ parseTag: function(){
+ // ast-filter look-ahead
+ var i = 2;
+ if ('attrs' == this.lookahead(i).type) ++i;
+ if (':' == this.lookahead(i).type) {
+ if ('indent' == this.lookahead(++i).type) {
+ return this.parseASTFilter();
+ }
+ }
+
+ var tok = this.advance()
+ , tag = new nodes.Tag(tok.val);
+
+ tag.selfClosing = tok.selfClosing;
+
+ return this.tag(tag);
+ },
+
+ /**
+ * Parse tag.
+ */
+
+ tag: function(tag){
+ var dot;
+
+ tag.line = this.line();
+
+ // (attrs | class | id)*
+ out:
+ while (true) {
+ switch (this.peek().type) {
+ case 'id':
+ case 'class':
+ var tok = this.advance();
+ tag.setAttribute(tok.type, "'" + tok.val + "'");
+ continue;
+ case 'attrs':
+ var tok = this.advance()
+ , obj = tok.attrs
+ , escaped = tok.escaped
+ , names = Object.keys(obj);
+
+ if (tok.selfClosing) tag.selfClosing = true;
+
+ for (var i = 0, len = names.length; i < len; ++i) {
+ var name = names[i]
+ , val = obj[name];
+ tag.setAttribute(name, val, escaped[name]);
+ }
+ continue;
+ default:
+ break out;
+ }
+ }
+
+ // check immediate '.'
+ if ('.' == this.peek().val) {
+ dot = tag.textOnly = true;
+ this.advance();
+ }
+
+ // (text | code | ':')?
+ switch (this.peek().type) {
+ case 'text':
+ tag.block.push(this.parseText());
+ break;
+ case 'code':
+ tag.code = this.parseCode();
+ break;
+ case ':':
+ this.advance();
+ tag.block = new nodes.Block;
+ tag.block.push(this.parseExpr());
+ break;
+ }
+
+ // newline*
+ while ('newline' == this.peek().type) this.advance();
+
+ tag.textOnly = tag.textOnly || ~textOnly.indexOf(tag.name);
+
+ // script special-case
+ if ('script' == tag.name) {
+ var type = tag.getAttribute('type');
+ if (!dot && type && 'text/javascript' != type.replace(/^['"]|['"]$/g, '')) {
+ tag.textOnly = false;
+ }
+ }
+
+ // block?
+ if ('indent' == this.peek().type) {
+ if (tag.textOnly) {
+ this.lexer.pipeless = true;
+ tag.block = this.parseTextBlock();
+ this.lexer.pipeless = false;
+ } else {
+ var block = this.block();
+ if (tag.block) {
+ for (var i = 0, len = block.nodes.length; i < len; ++i) {
+ tag.block.push(block.nodes[i]);
+ }
+ } else {
+ tag.block = block;
+ }
+ }
+ }
+
+ return tag;
+ }
+};