import { Lexer } from "./lexer"; export function Parser(expression) { this.expression = expression; this.lexer = new Lexer(expression); this.tokens = this.lexer.tokenize(); this.index = 0; } Parser.prototype = { getAst: function() { return this.start(); }, start: function() { try { return this.functionCall() || this.metricExpression(); } catch (e) { return { type: "error", message: e.message, pos: e.pos }; } }, curlyBraceSegment: function() { if (this.match("identifier", "{") || this.match("{")) { var curlySegment = ""; while (!this.match("") && !this.match("}")) { curlySegment += this.consumeToken().value; } if (!this.match("}")) { this.errorMark("Expected closing '}'"); } curlySegment += this.consumeToken().value; // if curly segment is directly followed by identifier // include it in the segment if (this.match("identifier")) { curlySegment += this.consumeToken().value; } return { type: "segment", value: curlySegment }; } else { return null; } }, metricSegment: function() { var curly = this.curlyBraceSegment(); if (curly) { return curly; } if (this.match("identifier") || this.match("number")) { // hack to handle float numbers in metric segments var parts = this.consumeToken().value.split("."); if (parts.length === 2) { this.tokens.splice(this.index, 0, { type: "." }); this.tokens.splice(this.index + 1, 0, { type: "number", value: parts[1] }); } return { type: "segment", value: parts[0] }; } if (!this.match("templateStart")) { this.errorMark("Expected metric identifier"); } this.consumeToken(); if (!this.match("identifier")) { this.errorMark("Expected identifier after templateStart"); } var node = { type: "template", value: this.consumeToken().value }; if (!this.match("templateEnd")) { this.errorMark("Expected templateEnd"); } this.consumeToken(); return node; }, metricExpression: function() { if ( !this.match("templateStart") && !this.match("identifier") && !this.match("number") && !this.match("{") ) { return null; } var node = { type: "metric", segments: [] }; node.segments.push(this.metricSegment()); while (this.match(".")) { this.consumeToken(); var segment = this.metricSegment(); if (!segment) { this.errorMark("Expected metric identifier"); } node.segments.push(segment); } return node; }, functionCall: function() { if (!this.match("identifier", "(")) { return null; } var node: any = { type: "function", name: this.consumeToken().value }; // consume left parenthesis this.consumeToken(); node.params = this.functionParameters(); if (!this.match(")")) { this.errorMark("Expected closing parenthesis"); } this.consumeToken(); return node; }, boolExpression: function() { if (!this.match("bool")) { return null; } return { type: "bool", value: this.consumeToken().value === "true" }; }, functionParameters: function() { if (this.match(")") || this.match("")) { return []; } var param = this.functionCall() || this.numericLiteral() || this.seriesRefExpression() || this.boolExpression() || this.metricExpression() || this.stringLiteral(); if (!this.match(",")) { return [param]; } this.consumeToken(); return [param].concat(this.functionParameters()); }, seriesRefExpression: function() { if (!this.match("identifier")) { return null; } var value = this.tokens[this.index].value; if (!value.match(/\#[A-Z]/)) { return null; } var token = this.consumeToken(); return { type: "series-ref", value: token.value }; }, numericLiteral: function() { if (!this.match("number")) { return null; } return { type: "number", value: parseFloat(this.consumeToken().value) }; }, stringLiteral: function() { if (!this.match("string")) { return null; } var token = this.consumeToken(); if (token.isUnclosed) { throw { message: "Unclosed string parameter", pos: token.pos }; } return { type: "string", value: token.value }; }, errorMark: function(text) { var currentToken = this.tokens[this.index]; var type = currentToken ? currentToken.type : "end of string"; throw { message: text + " instead found " + type, pos: currentToken ? currentToken.pos : this.lexer.char }; }, // returns token value and incre consumeToken: function() { this.index++; return this.tokens[this.index - 1]; }, matchToken: function(type, index) { var token = this.tokens[this.index + index]; return ( (token === undefined && type === "") || (token && token.type === type) ); }, match: function(token1, token2) { return ( this.matchToken(token1, 0) && (!token2 || this.matchToken(token2, 1)) ); } };