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)); }, };