pixsy/node_modules/pug-parser/index.js

1193 lines
28 KiB
JavaScript
Raw Normal View History

2017-12-22 17:22:08 +00:00
'use strict';
var assert = require('assert');
var TokenStream = require('token-stream');
var error = require('pug-error');
var inlineTags = require('./lib/inline-tags');
module.exports = parse;
module.exports.Parser = Parser;
function parse(tokens, options) {
var parser = new Parser(tokens, options);
var ast = parser.parse();
return JSON.parse(JSON.stringify(ast));
};
/**
* Initialize `Parser` with the given input `str` and `filename`.
*
* @param {String} str
* @param {String} filename
* @param {Object} options
* @api public
*/
function Parser(tokens, options) {
options = options || {};
if (!Array.isArray(tokens)) {
throw new Error('Expected tokens to be an Array but got "' + (typeof tokens) + '"');
}
if (typeof options !== 'object') {
throw new Error('Expected "options" to be an object but got "' + (typeof options) + '"');
}
this.tokens = new TokenStream(tokens);
this.filename = options.filename;
this.src = options.src;
this.inMixin = 0;
this.plugins = options.plugins || [];
};
/**
* Parser prototype.
*/
Parser.prototype = {
/**
* Save original constructor
*/
constructor: Parser,
error: function (code, message, token) {
var err = error(code, message, {
line: token.line,
column: token.col,
filename: this.filename,
src: this.src
});
throw err;
},
/**
* Return the next token object.
*
* @return {Object}
* @api private
*/
advance: function(){
return this.tokens.advance();
},
/**
* Single token lookahead.
*
* @return {Object}
* @api private
*/
peek: function() {
return this.tokens.peek();
},
/**
* `n` token lookahead.
*
* @param {Number} n
* @return {Object}
* @api private
*/
lookahead: function(n){
return this.tokens.lookahead(n);
},
/**
* Parse input returning a string of js for evaluation.
*
* @return {String}
* @api public
*/
parse: function(){
var block = this.emptyBlock(0);
while ('eos' != this.peek().type) {
if ('newline' == this.peek().type) {
this.advance();
} else if ('text-html' == this.peek().type) {
block.nodes = block.nodes.concat(this.parseTextHtml());
} else {
var expr = this.parseExpr();
if (expr) {
if (expr.type === 'Block') {
block.nodes = block.nodes.concat(expr.nodes);
} else {
block.nodes.push(expr);
}
}
}
}
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 {
this.error('INVALID_TOKEN', 'expected "' + type + '", but got "' + this.peek().type + '"', this.peek());
}
},
/**
* Accept the given `type`.
*
* @param {String} type
* @api private
*/
accept: function(type){
if (this.peek().type === type) {
return this.advance();
}
},
initBlock: function(line, nodes) {
/* istanbul ignore if */
if ((line | 0) !== line) throw new Error('`line` is not an integer');
/* istanbul ignore if */
if (!Array.isArray(nodes)) throw new Error('`nodes` is not an array');
return {
type: 'Block',
nodes: nodes,
line: line,
filename: this.filename
};
},
emptyBlock: function(line) {
return this.initBlock(line, []);
},
runPlugin: function(context, tok) {
var rest = [this];
for (var i = 2; i < arguments.length; i++) {
rest.push(arguments[i]);
}
var pluginContext;
for (var i = 0; i < this.plugins.length; i++) {
var plugin = this.plugins[i];
if (plugin[context] && plugin[context][tok.type]) {
if (pluginContext) throw new Error('Multiple plugin handlers found for context ' + JSON.stringify(context) + ', token type ' + JSON.stringify(tok.type));
pluginContext = plugin[context];
}
}
if (pluginContext) return pluginContext[tok.type].apply(pluginContext, rest);
},
/**
* tag
* | doctype
* | mixin
* | include
* | filter
* | comment
* | text
* | text-html
* | dot
* | 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':
case 'interpolated-code':
case 'start-pug-interpolation':
return this.parseText({block: true});
case 'text-html':
return this.initBlock(this.peek().line, this.parseTextHtml());
case 'dot':
return this.parseDot();
case 'each':
return this.parseEach();
case 'code':
return this.parseCode();
case 'blockcode':
return this.parseBlockCode();
case 'if':
return this.parseConditional();
case 'while':
return this.parseWhile();
case 'call':
return this.parseCall();
case 'interpolation':
return this.parseInterpolation();
case 'yield':
return this.parseYield();
case 'id':
case 'class':
this.tokens.defer({
type: 'tag',
val: 'div',
line: this.peek().line,
col: this.peek().col,
filename: this.filename
});
return this.parseExpr();
default:
var pluginResult = this.runPlugin('expressionTokens', this.peek());
if (pluginResult) return pluginResult;
this.error('INVALID_TOKEN', 'unexpected token "' + this.peek().type + '"', this.peek());
}
},
parseDot: function() {
this.advance();
return this.parseTextBlock();
},
/**
* Text
*/
parseText: function(options){
var tags = [];
var lineno = this.peek().line;
var nextTok = this.peek();
loop:
while (true) {
switch (nextTok.type) {
case 'text':
var tok = this.advance();
tags.push({
type: 'Text',
val: tok.val,
line: tok.line,
column: tok.col,
filename: this.filename
});
break;
case 'interpolated-code':
var tok = this.advance();
tags.push({
type: 'Code',
val: tok.val,
buffer: tok.buffer,
mustEscape: tok.mustEscape !== false,
isInline: true,
line: tok.line,
column: tok.col,
filename: this.filename
});
break;
case 'newline':
if (!options || !options.block) break loop;
var tok = this.advance();
var nextType = this.peek().type;
if (nextType === 'text' || nextType === 'interpolated-code') {
tags.push({
type: 'Text',
val: '\n',
line: tok.line,
column: tok.col,
filename: this.filename
});
}
break;
case 'start-pug-interpolation':
this.advance();
tags.push(this.parseExpr());
this.expect('end-pug-interpolation');
break;
default:
var pluginResult = this.runPlugin('textTokens', nextTok, tags);
if (pluginResult) break;
break loop;
}
nextTok = this.peek();
}
if (tags.length === 1) return tags[0];
else return this.initBlock(lineno, tags);
},
parseTextHtml: function () {
var nodes = [];
var currentNode = null;
loop:
while (true) {
switch (this.peek().type) {
case 'text-html':
var text = this.advance();
if (!currentNode) {
currentNode = {
type: 'Text',
val: text.val,
filename: this.filename,
line: text.line,
column: text.col,
isHtml: true
};
nodes.push(currentNode);
} else {
currentNode.val += '\n' + text.val;
}
break;
case 'indent':
var block = this.block();
block.nodes.forEach(function (node) {
if (node.isHtml) {
if (!currentNode) {
currentNode = node;
nodes.push(currentNode);
} else {
currentNode.val += '\n' + node.val;
}
} else {
currentNode = null;
nodes.push(node);
}
});
break;
case 'code':
currentNode = null;
nodes.push(this.parseCode(true));
break;
case 'newline':
this.advance();
break;
default:
break loop;
}
}
return nodes;
},
/**
* ':' expr
* | block
*/
parseBlockExpansion: function(){
var tok = this.accept(':');
if (tok) {
var expr = this.parseExpr();
return expr.type === 'Block' ? expr : this.initBlock(tok.line, [expr]);
} else {
return this.block();
}
},
/**
* case
*/
parseCase: function(){
var tok = this.expect('case');
var node = {
type: 'Case',
expr: tok.val,
line: tok.line,
column: tok.col,
filename: this.filename
};
var block = this.emptyBlock(tok.line + 1);
this.expect('indent');
while ('outdent' != this.peek().type) {
switch (this.peek().type) {
case 'comment':
case 'newline':
this.advance();
break;
case 'when':
block.nodes.push(this.parseWhen());
break;
case 'default':
block.nodes.push(this.parseDefault());
break;
default:
var pluginResult = this.runPlugin('caseTokens', this.peek(), block);
if (pluginResult) break;
this.error('INVALID_TOKEN', 'Unexpected token "' + this.peek().type
+ '", expected "when", "default" or "newline"', this.peek());
}
}
this.expect('outdent');
node.block = block;
return node;
},
/**
* when
*/
parseWhen: function(){
var tok = this.expect('when');
if (this.peek().type !== 'newline') {
return {
type: 'When',
expr: tok.val,
block: this.parseBlockExpansion(),
debug: false,
line: tok.line,
column: tok.col,
filename: this.filename
};
} else {
return {
type: 'When',
expr: tok.val,
debug: false,
line: tok.line,
column: tok.col,
filename: this.filename
};
}
},
/**
* default
*/
parseDefault: function(){
var tok = this.expect('default');
return {
type: 'When',
expr: 'default',
block: this.parseBlockExpansion(),
debug: false,
line: tok.line,
column: tok.col,
filename: this.filename
};
},
/**
* code
*/
parseCode: function(noBlock){
var tok = this.expect('code');
assert(typeof tok.mustEscape === 'boolean', 'Please update to the newest version of pug-lexer.');
var node = {
type: 'Code',
val: tok.val,
buffer: tok.buffer,
mustEscape: tok.mustEscape !== false,
isInline: !!noBlock,
line: tok.line,
column: tok.col,
filename: this.filename
};
// todo: why is this here? It seems like a hacky workaround
if (node.val.match(/^ *else/)) node.debug = false;
if (noBlock) return node;
var block;
// handle block
block = 'indent' == this.peek().type;
if (block) {
if (tok.buffer) {
this.error('BLOCK_IN_BUFFERED_CODE', 'Buffered code cannot have a block attached to it', this.peek());
}
node.block = this.block();
}
return node;
},
parseConditional: function(){
var tok = this.expect('if');
var node = {
type: 'Conditional',
test: tok.val,
consequent: this.emptyBlock(tok.line),
alternate: null,
line: tok.line,
column: tok.col,
filename: this.filename
};
// handle block
if ('indent' == this.peek().type) {
node.consequent = this.block();
}
var currentNode = node;
while (true) {
if (this.peek().type === 'newline') {
this.expect('newline');
} else if (this.peek().type === 'else-if') {
tok = this.expect('else-if');
currentNode = (
currentNode.alternate = {
type: 'Conditional',
test: tok.val,
consequent: this.emptyBlock(tok.line),
alternate: null,
line: tok.line,
column: tok.col,
filename: this.filename
}
);
if ('indent' == this.peek().type) {
currentNode.consequent = this.block();
}
} else if (this.peek().type === 'else') {
this.expect('else');
if (this.peek().type === 'indent') {
currentNode.alternate = this.block();
}
break;
} else {
break;
}
}
return node;
},
parseWhile: function(){
var tok = this.expect('while');
var node = {
type: 'While',
test: tok.val,
line: tok.line,
column: tok.col,
filename: this.filename
};
// handle block
if ('indent' == this.peek().type) {
node.block = this.block();
} else {
node.block = this.emptyBlock(tok.line);
}
return node;
},
/**
* block code
*/
parseBlockCode: function(){
var tok = this.expect('blockcode');
var line = tok.line;
var column = tok.col;
var body = this.peek();
var text = '';
if (body.type === 'start-pipeless-text') {
this.advance();
while (this.peek().type !== 'end-pipeless-text') {
tok = this.advance();
switch (tok.type) {
case 'text':
text += tok.val;
break;
case 'newline':
text += '\n';
break;
default:
var pluginResult = this.runPlugin('blockCodeTokens', tok, tok);
if (pluginResult) {
text += pluginResult;
break;
}
this.error('INVALID_TOKEN', 'Unexpected token type: ' + tok.type, tok);
}
}
this.advance();
}
return {
type: 'Code',
val: text,
buffer: false,
mustEscape: false,
isInline: false,
line: line,
column: column,
filename: this.filename
};
},
/**
* comment
*/
parseComment: function(){
var tok = this.expect('comment');
var block;
if (block = this.parseTextBlock()) {
return {
type: 'BlockComment',
val: tok.val,
block: block,
buffer: tok.buffer,
line: tok.line,
column: tok.col,
filename: this.filename
};
} else {
return {
type: 'Comment',
val: tok.val,
buffer: tok.buffer,
line: tok.line,
column: tok.col,
filename: this.filename
};
}
},
/**
* doctype
*/
parseDoctype: function(){
var tok = this.expect('doctype');
return {
type: 'Doctype',
val: tok.val,
line: tok.line,
column: tok.col,
filename: this.filename
};
},
parseIncludeFilter: function() {
var tok = this.expect('filter');
var attrs = [];
if (this.peek().type === 'start-attributes') {
attrs = this.attrs();
}
return {
type: 'IncludeFilter',
name: tok.val,
attrs: attrs,
line: tok.line,
column: tok.col,
filename: this.filename
};
},
/**
* filter attrs? text-block
*/
parseFilter: function(){
var tok = this.expect('filter');
var block, attrs = [];
if (this.peek().type === 'start-attributes') {
attrs = this.attrs();
}
if (this.peek().type === 'text') {
var textToken = this.advance();
block = this.initBlock(textToken.line, [
{
type: 'Text',
val: textToken.val,
line: textToken.line,
column: textToken.col,
filename: this.filename
}
]);
} else if (this.peek().type === 'filter') {
block = this.initBlock(tok.line, [this.parseFilter()]);
} else {
block = this.parseTextBlock() || this.emptyBlock(tok.line);
}
return {
type: 'Filter',
name: tok.val,
block: block,
attrs: attrs,
line: tok.line,
column: tok.col,
filename: this.filename
};
},
/**
* each block
*/
parseEach: function(){
var tok = this.expect('each');
var node = {
type: 'Each',
obj: tok.code,
val: tok.val,
key: tok.key,
block: this.block(),
line: tok.line,
column: tok.col,
filename: this.filename
};
if (this.peek().type == 'else') {
this.advance();
node.alternate = this.block();
}
return node;
},
/**
* 'extends' name
*/
parseExtends: function(){
var tok = this.expect('extends');
var path = this.expect('path');
return {
type: 'Extends',
file: {
type: 'FileReference',
path: path.val.trim(),
line: path.line,
column: path.col,
filename: this.filename
},
line: tok.line,
column: tok.col,
filename: this.filename
};
},
/**
* 'block' name block
*/
parseBlock: function(){
var tok = this.expect('block');
var node = 'indent' == this.peek().type ? this.block() : this.emptyBlock(tok.line);
node.type = 'NamedBlock';
node.name = tok.val.trim();
node.mode = tok.mode;
node.line = tok.line;
node.column = tok.col;
return node;
},
parseMixinBlock: function () {
var tok = this.expect('mixin-block');
if (!this.inMixin) {
this.error('BLOCK_OUTISDE_MIXIN', 'Anonymous blocks are not allowed unless they are part of a mixin.', tok);
}
return {
type: 'MixinBlock',
line: tok.line,
column: tok.col,
filename: this.filename
};
},
parseYield: function() {
var tok = this.expect('yield');
return {
type: 'YieldBlock',
line: tok.line,
column: tok.col,
filename: this.filename
};
},
/**
* include block?
*/
parseInclude: function(){
var tok = this.expect('include');
var node = {
type: 'Include',
file: {
type: 'FileReference',
filename: this.filename
},
line: tok.line,
column: tok.col,
filename: this.filename
};
var filters = [];
while (this.peek().type === 'filter') {
filters.push(this.parseIncludeFilter());
}
var path = this.expect('path');
node.file.path = path.val.trim();
node.file.line = path.line;
node.file.column = path.col;
if ((/\.jade$/.test(node.file.path) || /\.pug$/.test(node.file.path)) && !filters.length) {
node.block = 'indent' == this.peek().type ? this.block() : this.emptyBlock(tok.line);
if (/\.jade$/.test(node.file.path)) {
console.warn(
this.filename + ', line ' + tok.line +
':\nThe .jade extension is deprecated, use .pug for "' + node.file.path +'".'
);
}
} else {
node.type = 'RawInclude';
node.filters = filters;
if (this.peek().type === 'indent') {
this.error('RAW_INCLUDE_BLOCK', 'Raw inclusion cannot contain a block', this.peek());
}
}
return node;
},
/**
* call ident block
*/
parseCall: function(){
var tok = this.expect('call');
var name = tok.val;
var args = tok.args;
var mixin = {
type: 'Mixin',
name: name,
args: args,
block: this.emptyBlock(tok.line),
call: true,
attrs: [],
attributeBlocks: [],
line: tok.line,
column: tok.col,
filename: this.filename
};
this.tag(mixin);
if (mixin.code) {
mixin.block.nodes.push(mixin.code);
delete mixin.code;
}
if (mixin.block.nodes.length === 0) mixin.block = null;
return mixin;
},
/**
* mixin block
*/
parseMixin: function(){
var tok = this.expect('mixin');
var name = tok.val;
var args = tok.args;
if ('indent' == this.peek().type) {
this.inMixin++;
var mixin = {
type: 'Mixin',
name: name,
args: args,
block: this.block(),
call: false,
line: tok.line,
column: tok.col,
filename: this.filename
};
this.inMixin--;
return mixin;
} else {
this.error('MIXIN_WITHOUT_BODY', 'Mixin ' + name + ' declared without body', tok);
}
},
/**
* indent (text | newline)* outdent
*/
parseTextBlock: function(){
var tok = this.accept('start-pipeless-text');
if (!tok) return;
var block = this.emptyBlock(tok.line);
while (this.peek().type !== 'end-pipeless-text') {
var tok = this.advance();
switch (tok.type) {
case 'text':
block.nodes.push({
type: 'Text',
val: tok.val,
line: tok.line,
column: tok.col,
filename: this.filename
});
break;
case 'newline':
block.nodes.push({
type: 'Text',
val: '\n',
line: tok.line,
column: tok.col,
filename: this.filename
});
break;
case 'start-pug-interpolation':
block.nodes.push(this.parseExpr());
this.expect('end-pug-interpolation');
break;
case 'interpolated-code':
block.nodes.push({
type: 'Code',
val: tok.val,
buffer: tok.buffer,
mustEscape: tok.mustEscape !== false,
isInline: true,
line: tok.line,
column: tok.col,
filename: this.filename
});
break;
default:
var pluginResult = this.runPlugin('textBlockTokens', tok, block, tok);
if (pluginResult) break;
this.error('INVALID_TOKEN', 'Unexpected token type: ' + tok.type, tok);
}
}
this.advance();
return block;
},
/**
* indent expr* outdent
*/
block: function(){
var tok = this.expect('indent');
var block = this.emptyBlock(tok.line);
while ('outdent' != this.peek().type) {
if ('newline' == this.peek().type) {
this.advance();
} else if ('text-html' == this.peek().type) {
block.nodes = block.nodes.concat(this.parseTextHtml());
} else {
var expr = this.parseExpr();
if (expr.type === 'Block') {
block.nodes = block.nodes.concat(expr.nodes);
} else {
block.nodes.push(expr);
}
}
}
this.expect('outdent');
return block;
},
/**
* interpolation (attrs | class | id)* (text | code | ':')? newline* block?
*/
parseInterpolation: function(){
var tok = this.advance();
var tag = {
type: 'InterpolatedTag',
expr: tok.val,
selfClosing: false,
block: this.emptyBlock(tok.line),
attrs: [],
attributeBlocks: [],
isInline: false,
line: tok.line,
column: tok.col,
filename: this.filename
};
return this.tag(tag, {selfClosingAllowed: true});
},
/**
* tag (attrs | class | id)* (text | code | ':')? newline* block?
*/
parseTag: function(){
var tok = this.advance();
var tag = {
type: 'Tag',
name: tok.val,
selfClosing: false,
block: this.emptyBlock(tok.line),
attrs: [],
attributeBlocks: [],
isInline: inlineTags.indexOf(tok.val) !== -1,
line: tok.line,
column: tok.col,
filename: this.filename
};
return this.tag(tag, {selfClosingAllowed: true});
},
/**
* Parse tag.
*/
tag: function(tag, options) {
var seenAttrs = false;
var attributeNames = [];
var selfClosingAllowed = options && options.selfClosingAllowed;
// (attrs | class | id)*
out:
while (true) {
switch (this.peek().type) {
case 'id':
case 'class':
var tok = this.advance();
if (tok.type === 'id') {
if (attributeNames.indexOf('id') !== -1) {
this.error('DUPLICATE_ID', 'Duplicate attribute "id" is not allowed.', tok);
}
attributeNames.push('id');
}
tag.attrs.push({
name: tok.type,
val: "'" + tok.val + "'",
line: tok.line,
column: tok.col,
filename: this.filename,
mustEscape: false
});
continue;
case 'start-attributes':
if (seenAttrs) {
console.warn(this.filename + ', line ' + this.peek().line + ':\nYou should not have pug tags with multiple attributes.');
}
seenAttrs = true;
tag.attrs = tag.attrs.concat(this.attrs(attributeNames));
continue;
case '&attributes':
var tok = this.advance();
tag.attributeBlocks.push({
type: 'AttributeBlock',
val: tok.val,
line: tok.line,
column: tok.col,
filename: this.filename
});
break;
default:
var pluginResult = this.runPlugin('tagAttributeTokens', this.peek(), tag, attributeNames);
if (pluginResult) break;
break out;
}
}
// check immediate '.'
if ('dot' == this.peek().type) {
tag.textOnly = true;
this.advance();
}
// (text | code | ':')?
switch (this.peek().type) {
case 'text':
case 'interpolated-code':
var text = this.parseText();
if (text.type === 'Block') {
tag.block.nodes.push.apply(tag.block.nodes, text.nodes);
} else {
tag.block.nodes.push(text);
}
break;
case 'code':
tag.block.nodes.push(this.parseCode(true));
break;
case ':':
this.advance();
var expr = this.parseExpr();
tag.block = expr.type === 'Block' ? expr : this.initBlock(tag.line, [expr]);
break;
case 'newline':
case 'indent':
case 'outdent':
case 'eos':
case 'start-pipeless-text':
case 'end-pug-interpolation':
break;
case 'slash':
if (selfClosingAllowed) {
this.advance();
tag.selfClosing = true;
break;
}
default:
var pluginResult = this.runPlugin('tagTokens', this.peek(), tag, options);
if (pluginResult) break;
this.error('INVALID_TOKEN', 'Unexpected token `' + this.peek().type + '` expected `text`, `interpolated-code`, `code`, `:`' + (selfClosingAllowed ? ', `slash`' : '') + ', `newline` or `eos`', this.peek())
}
// newline*
while ('newline' == this.peek().type) this.advance();
// block?
if (tag.textOnly) {
tag.block = this.parseTextBlock() || this.emptyBlock(tag.line);
} else if ('indent' == this.peek().type) {
var block = this.block();
for (var i = 0, len = block.nodes.length; i < len; ++i) {
tag.block.nodes.push(block.nodes[i]);
}
}
return tag;
},
attrs: function(attributeNames) {
this.expect('start-attributes');
var attrs = [];
var tok = this.advance();
while (tok.type === 'attribute') {
if (tok.name !== 'class' && attributeNames) {
if (attributeNames.indexOf(tok.name) !== -1) {
this.error('DUPLICATE_ATTRIBUTE', 'Duplicate attribute "' + tok.name + '" is not allowed.', tok);
}
attributeNames.push(tok.name);
}
attrs.push({
name: tok.name,
val: tok.val,
line: tok.line,
column: tok.col,
filename: this.filename,
mustEscape: tok.mustEscape !== false
});
tok = this.advance();
}
this.tokens.defer(tok);
this.expect('end-attributes');
return attrs;
}
};