|
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824 |
- 'use strict';
-
- var Lexer = require('./lexer');
- var nodes = require('./nodes');
- var utils = require('./utils');
- var filters = require('./filters');
- var path = require('path');
- var constantinople = require('constantinople');
- var parseJSExpression = require('character-parser').parseMax;
- var extname = path.extname;
-
- /**
- * 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){
- //Strip any UTF-8 BOM off of the start of `str`, if it exists.
- this.input = str.replace(/^\uFEFF/, '');
- this.lexer = new Lexer(this.input, filename);
- this.filename = filename;
- this.blocks = {};
- this.mixins = {};
- this.options = options;
- this.contexts = [this];
- this.inMixin = false;
- this.dependencies = [];
- this.inBlock = 0;
- };
-
- /**
- * Parser prototype.
- */
-
- Parser.prototype = {
-
- /**
- * Save original constructor
- */
-
- constructor: Parser,
-
- /**
- * 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();
- },
-
- /**
- * 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 = 0;
- block.filename = this.filename;
-
- while ('eos' != this.peek().type) {
- if ('newline' == this.peek().type) {
- this.advance();
- } else {
- var next = this.peek();
- var expr = this.parseExpr();
- expr.filename = expr.filename || this.filename;
- expr.line = next.line;
- block.push(expr);
- }
- }
-
- 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;
- }
-
- if (!this.extending && !this.included && Object.keys(this.blocks).length){
- var blocks = [];
- utils.walkAST(block, function (node) {
- if (node.type === 'Block' && node.name) {
- blocks.push(node.name);
- }
- });
- Object.keys(this.blocks).forEach(function (name) {
- if (blocks.indexOf(name) === -1 && !this.blocks[name].isSubBlock) {
- console.warn('Warning: Unexpected block "'
- + name
- + '" '
- + ' on line '
- + this.blocks[name].line
- + ' of '
- + (this.blocks[name].filename)
- + '. This block is never used. This warning will be an error in v2.0.0');
- }
- }.bind(this));
- }
-
- 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 'mixin-block':
- return this.parseMixinBlock();
- case 'case':
- return this.parseCase();
- 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');
- var tokens = this.parseInlineTagsInText(tok.val);
- if (tokens.length === 1) return tokens[0];
- var node = new nodes.Block;
- for (var i = 0; i < tokens.length; i++) {
- node.push(tokens[i]);
- };
- 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;
- var node = new nodes.Case(val);
- node.line = this.line();
-
- var block = new nodes.Block;
- block.line = this.line();
- block.filename = this.filename;
- this.expect('indent');
- while ('outdent' != this.peek().type) {
- switch (this.peek().type) {
- case 'newline':
- this.advance();
- break;
- case 'when':
- block.push(this.parseWhen());
- break;
- case 'default':
- block.push(this.parseDefault());
- break;
- default:
- throw new Error('Unexpected token "' + this.peek().type
- + '", expected "when", "default" or "newline"');
- }
- }
- this.expect('outdent');
-
- node.block = block;
-
- return node;
- },
-
- /**
- * when
- */
-
- parseWhen: function(){
- var val = this.expect('when').val;
- if (this.peek().type !== 'newline')
- return new nodes.Case.When(val, this.parseBlockExpansion());
- else
- return new nodes.Case.When(val);
- },
-
- /**
- * default
- */
-
- parseDefault: function(){
- this.expect('default');
- return new nodes.Case.When('default', this.parseBlockExpansion());
- },
-
- /**
- * code
- */
-
- parseCode: function(afterIf){
- var tok = this.expect('code');
- var node = new nodes.Code(tok.val, tok.buffer, tok.escape);
- var block;
- node.line = this.line();
-
- // throw an error if an else does not have an if
- if (tok.isElse && !tok.hasIf) {
- throw new Error('Unexpected else without if');
- }
-
- // handle block
- block = 'indent' == this.peek().type;
- if (block) {
- node.block = this.block();
- }
-
- // handle missing block
- if (tok.requiresBlock && !block) {
- node.block = new nodes.Block();
- }
-
- // mark presense of if for future elses
- if (tok.isIf && this.peek().isElse) {
- this.peek().hasIf = true;
- } else if (tok.isIf && this.peek().type === 'newline' && this.lookahead(2).isElse) {
- this.lookahead(2).hasIf = true;
- }
-
- return node;
- },
-
- /**
- * comment
- */
-
- parseComment: function(){
- var tok = this.expect('comment');
- var node;
-
- var block;
- if (block = this.parseTextBlock()) {
- node = new nodes.BlockComment(tok.val, 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');
- var node = new nodes.Doctype(tok.val);
- node.line = this.line();
- return node;
- },
-
- /**
- * filter attrs? text-block
- */
-
- parseFilter: function(){
- var tok = this.expect('filter');
- var attrs = this.accept('attrs');
- var block;
-
- block = this.parseTextBlock() || new nodes.Block();
-
- var options = {};
- if (attrs) {
- attrs.attrs.forEach(function (attribute) {
- options[attribute.name] = constantinople.toConstant(attribute.val);
- });
- }
-
- var node = new nodes.Filter(tok.val, block, options);
- node.line = this.line();
- return node;
- },
-
- /**
- * each block
- */
-
- parseEach: function(){
- var tok = this.expect('each');
- var node = new nodes.Each(tok.code, tok.val, tok.key);
- node.line = this.line();
- node.block = this.block();
- if (this.peek().type == 'code' && this.peek().val == 'else') {
- this.advance();
- node.alternative = this.block();
- }
- return node;
- },
-
- /**
- * Resolves a path relative to the template for use in
- * includes and extends
- *
- * @param {String} path
- * @param {String} purpose Used in error messages.
- * @return {String}
- * @api private
- */
-
- resolvePath: function (path, purpose) {
- var p = require('path');
- var dirname = p.dirname;
- var basename = p.basename;
- var join = p.join;
-
- if (path[0] !== '/' && !this.filename)
- throw new Error('the "filename" option is required to use "' + purpose + '" with "relative" paths');
-
- if (path[0] === '/' && !this.options.basedir)
- throw new Error('the "basedir" option is required to use "' + purpose + '" with "absolute" paths');
-
- path = join(path[0] === '/' ? this.options.basedir : dirname(this.filename), path);
-
- if (basename(path).indexOf('.') === -1) path += '.jade';
-
- return path;
- },
-
- /**
- * 'extends' name
- */
-
- parseExtends: function(){
- var fs = require('fs');
-
- var path = this.resolvePath(this.expect('extends').val.trim(), 'extends');
- if ('.jade' != path.substr(-5)) path += '.jade';
-
- this.dependencies.push(path);
- var str = fs.readFileSync(path, 'utf8');
- var parser = new this.constructor(str, path, this.options);
- parser.dependencies = this.dependencies;
-
- parser.blocks = this.blocks;
- parser.included = this.included;
- parser.contexts = this.contexts;
- this.extending = parser;
-
- // TODO: null node
- return new nodes.Literal('');
- },
-
- /**
- * 'block' name block
- */
-
- parseBlock: function(){
- var block = this.expect('block');
- var mode = block.mode;
- var name = block.val.trim();
-
- var line = block.line;
-
- this.inBlock++;
- block = 'indent' == this.peek().type
- ? this.block()
- : new nodes.Block(new nodes.Literal(''));
- this.inBlock--;
- block.name = name;
- block.line = line;
-
- var prev = this.blocks[name] || {prepended: [], appended: []}
- if (prev.mode === 'replace') return this.blocks[name] = prev;
-
- var allNodes = prev.prepended.concat(block.nodes).concat(prev.appended);
-
- switch (mode) {
- case 'append':
- prev.appended = prev.parser === this ?
- prev.appended.concat(block.nodes) :
- block.nodes.concat(prev.appended);
- break;
- case 'prepend':
- prev.prepended = prev.parser === this ?
- block.nodes.concat(prev.prepended) :
- prev.prepended.concat(block.nodes);
- break;
- }
- block.nodes = allNodes;
- block.appended = prev.appended;
- block.prepended = prev.prepended;
- block.mode = mode;
- block.parser = this;
-
- block.isSubBlock = this.inBlock > 0;
-
- return this.blocks[name] = block;
- },
-
- parseMixinBlock: function () {
- var block = this.expect('mixin-block');
- if (!this.inMixin) {
- throw new Error('Anonymous blocks are not allowed unless they are part of a mixin.');
- }
- return new nodes.MixinBlock();
- },
-
- /**
- * include block?
- */
-
- parseInclude: function(){
- var fs = require('fs');
- var tok = this.expect('include');
-
- var path = this.resolvePath(tok.val.trim(), 'include');
- this.dependencies.push(path);
- // has-filter
- if (tok.filter) {
- var str = fs.readFileSync(path, 'utf8').replace(/\r/g, '');
- var options = {filename: path};
- if (tok.attrs) {
- tok.attrs.attrs.forEach(function (attribute) {
- options[attribute.name] = constantinople.toConstant(attribute.val);
- });
- }
- str = filters(tok.filter, str, options);
- return new nodes.Literal(str);
- }
-
- // non-jade
- if ('.jade' != path.substr(-5)) {
- var str = fs.readFileSync(path, 'utf8').replace(/\r/g, '');
- return new nodes.Literal(str);
- }
-
- var str = fs.readFileSync(path, 'utf8');
- var parser = new this.constructor(str, path, this.options);
- parser.dependencies = this.dependencies;
-
- parser.blocks = utils.merge({}, this.blocks);
- parser.included = true;
-
- 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');
- var name = tok.val;
- var args = tok.args;
- var mixin = new nodes.Mixin(name, args, new nodes.Block, true);
-
- this.tag(mixin);
- if (mixin.code) {
- mixin.block.push(mixin.code);
- mixin.code = null;
- }
- if (mixin.block.isEmpty()) mixin.block = null;
- return mixin;
- },
-
- /**
- * mixin block
- */
-
- parseMixin: function(){
- var tok = this.expect('mixin');
- var name = tok.val;
- var args = tok.args;
- var mixin;
-
- // definition
- if ('indent' == this.peek().type) {
- this.inMixin = true;
- mixin = new nodes.Mixin(name, args, this.block(), false);
- this.mixins[name] = mixin;
- this.inMixin = false;
- return mixin;
- // call
- } else {
- return new nodes.Mixin(name, args, null, true);
- }
- },
-
- parseInlineTagsInText: function (str) {
- var line = this.line();
-
- var match = /(\\)?#\[((?:.|\n)*)$/.exec(str);
- if (match) {
- if (match[1]) { // escape
- var text = new nodes.Text(str.substr(0, match.index) + '#[');
- text.line = line;
- var rest = this.parseInlineTagsInText(match[2]);
- if (rest[0].type === 'Text') {
- text.val += rest[0].val;
- rest.shift();
- }
- return [text].concat(rest);
- } else {
- var text = new nodes.Text(str.substr(0, match.index));
- text.line = line;
- var buffer = [text];
- var rest = match[2];
- var range = parseJSExpression(rest);
- var inner = new Parser(range.src, this.filename, this.options);
- buffer.push(inner.parse());
- return buffer.concat(this.parseInlineTagsInText(rest.substr(range.end + 1)));
- }
- } else {
- var text = new nodes.Text(str);
- text.line = line;
- return [text];
- }
- },
-
- /**
- * indent (text | newline)* outdent
- */
-
- parseTextBlock: function(){
- var block = new nodes.Block;
- block.line = this.line();
- var body = this.peek();
- if (body.type !== 'pipeless-text') return;
- this.advance();
- block.nodes = body.val.reduce(function (accumulator, text) {
- return accumulator.concat(this.parseInlineTagsInText(text));
- }.bind(this), []);
- return block;
- },
-
- /**
- * indent expr* outdent
- */
-
- block: function(){
- var block = new nodes.Block;
- block.line = this.line();
- block.filename = this.filename;
- this.expect('indent');
- while ('outdent' != this.peek().type) {
- if ('newline' == this.peek().type) {
- this.advance();
- } else {
- var expr = this.parseExpr();
- expr.filename = this.filename;
- block.push(expr);
- }
- }
- 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(){
- var tok = this.advance();
- var tag = new nodes.Tag(tok.val);
-
- tag.selfClosing = tok.selfClosing;
-
- return this.tag(tag);
- },
-
- /**
- * Parse tag.
- */
-
- tag: function(tag){
- tag.line = this.line();
-
- var seenAttrs = false;
- // (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':
- if (seenAttrs) {
- console.warn(this.filename + ', line ' + this.peek().line + ':\nYou should not have jade tags with multiple attributes.');
- }
- seenAttrs = true;
- var tok = this.advance();
- var attrs = tok.attrs;
-
- if (tok.selfClosing) tag.selfClosing = true;
-
- for (var i = 0; i < attrs.length; i++) {
- tag.setAttribute(attrs[i].name, attrs[i].val, attrs[i].escaped);
- }
- continue;
- case '&attributes':
- var tok = this.advance();
- tag.addAttributes(tok.val);
- break;
- default:
- break out;
- }
- }
-
- // check immediate '.'
- if ('dot' == this.peek().type) {
- 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;
- case 'newline':
- case 'indent':
- case 'outdent':
- case 'eos':
- case 'pipeless-text':
- break;
- default:
- throw new Error('Unexpected token `' + this.peek().type + '` expected `text`, `code`, `:`, `newline` or `eos`')
- }
-
- // newline*
- while ('newline' == this.peek().type) this.advance();
-
- // block?
- if (tag.textOnly) {
- tag.block = this.parseTextBlock();
- } else if ('indent' == this.peek().type) {
- var block = this.block();
- for (var i = 0, len = block.nodes.length; i < len; ++i) {
- tag.block.push(block.nodes[i]);
- }
- }
-
- return tag;
- }
- };
|