/**
* This class parses simple expressions. The parser can be enhanced by providing any of
* the following configs:
*
* * `constants`
* * `infix`
* * `infixRight`
* * `postfix`
* * `symbols`
*
* The parser requires a `{@link Ext.parse.Tokenizer tokenizer}` which can be configured
* using the `tokenizer` config. The parser keeps the tokenizer instance and recycles it
* as it is itself reused.
*
* See http://javascript.crockford.com/tdop/tdop.html for background on the techniques
* used in this parser.
* @private
*/
Ext.define('Ext.parse.Parser', function() {
var ITSELF = function() {
return this;
};
/* eslint-disable indent */
return {
extend: 'Ext.util.Fly',
requires: [
'Ext.parse.Tokenizer',
'Ext.parse.symbol.Constant',
'Ext.parse.symbol.InfixRight',
'Ext.parse.symbol.Paren',
'Ext.parse.symbol.Prefix'
],
isParser: true,
config: {
/**
* @cfg {Object} constants
* A map of identifiers that should be converted to literal value tokens. The
* key in this object is the name of the constant and the value is the constant
* value.
*
* If the value of a key is an object, it is a config object for the
* `{@link Ext.parse.symbol.Constant constant}`.
*/
constants: {
'null': null,
'false': false,
'true': true
},
/**
* @cfg {Object} infix
* A map of binary operators and their associated precedence (or binding priority).
* These binary operators are left-associative.
*
* If the value of a key is an object, it is a config object for the
* `{@link Ext.parse.symbol.Infix operator}`.
*/
infix: {
'===': 40,
'!==': 40,
'==': 40,
'!=': 40,
'<': 40,
'<=': 40,
'>': 40,
'>=': 40,
'+': 50,
'-': 50,
'*': 60,
'/': 60
},
/**
* @cfg {Object} infixRight
* A map of binary operators and their associated precedence (or binding priority).
* These binary operators are right-associative.
*
* If the value of a key is an object, it is a config object for the
* `{@link Ext.parse.symbol.InfixRight operator}`.
*/
infixRight: {
'&&': 30,
'||': 30
},
/**
* @cfg {Object} prefix
* A map of unary operators. Typically no value is needed, so `0` is used.
*
* If the value of a key is an object, it is a config object for the
* `{@link Ext.parse.symbol.Prefix operator}`.
*/
prefix: {
'!': 0,
'-': 0,
'+': 0
},
/**
* @cfg {Object} symbols
* General language symbols. The values in this object are used as config objects
* to configure the associated `{@link Ext.parse.Symbol symbol}`. If there is no
* configuration, use `0` for the value.
*/
symbols: {
':': 0,
',': 0,
')': 0,
'[': 0,
']': 0,
'{': 0,
'}': 0,
'(end)': 0,
'(ident)': {
arity: 'ident',
isIdent: true,
nud: ITSELF
},
'(literal)': {
arity: 'literal',
isLiteral: true,
nud: ITSELF
},
'(': {
xclass: 'Ext.parse.symbol.Paren'
}
},
/**
* @cfg {Object/Ext.parse.Tokenizer} tokenizer
* The tokenizer or a config object used to create one.
*/
tokenizer: {
keywords: null // we'll handle keywords here
}
},
/**
* @cfg {Ext.parse.Symbol} token
* The current token. These tokens extend this base class and contain additional
* properties such as:
*
* * `at` - The index of the token in the text.
* * `value` - The value of the token (e.g., the name of an identifier).
*
* @readonly
*/
token: null,
constructor: function(config) {
this.symbols = {};
this.initConfig(config);
},
/**
* Advances the token stream and returns the next `token`.
* @param {String} [expected] The type of symbol that is expected to follow.
* @return {Ext.parse.Symbol}
*/
advance: function(expected) {
var me = this,
tokenizer = me.tokenizer,
token = tokenizer.peek(),
symbols = me.symbols,
index = tokenizer.index,
is, name, symbol, value;
if (me.error) {
throw me.error;
}
if (expected) {
me.expect(expected);
}
if (!token) {
return me.token = symbols['(end)'];
}
tokenizer.next();
is = token.is;
value = token.value;
if (is.ident) {
symbol = symbols[value] || symbols['(ident)'];
}
else if (is.operator) {
if (!(symbol = symbols[value])) {
me.syntaxError(token.at, 'Unknown operator "' + value + '"');
}
name = token.name;
}
else if (is.literal) {
symbol = symbols['(literal)'];
}
else {
me.syntaxError(token.at, 'Unexpected token');
}
me.token = symbol = Ext.Object.chain(symbol);
symbol.at = index;
symbol.is = is;
symbol.value = value;
if (!symbol.arity) {
symbol.arity = token.type;
}
if (name) {
symbol.name = name;
}
return symbol;
},
expect: function(expected) {
var token = this.token;
if (expected !== token.id) {
this.syntaxError(token.at, 'Expected "' + expected + '"');
}
return this;
},
/**
*
* @param {Number} [rightPriority=0] The precedence of the current operator.
* @return {Ext.parse.Symbol} The parsed expression tree.
*/
parseExpression: function(rightPriority) {
var me = this,
token = me.token,
left;
rightPriority = rightPriority || 0;
me.advance();
left = token.nud();
while (rightPriority < (token = me.token).priority) {
me.advance();
left = token.led(left);
}
return left;
},
/**
* Resets this parser given the text to parse or a `Tokenizer`.
* @param {String} text
* @param {Number} [pos=0] The character position at which to start.
* @param {Number} [end] The index of the first character beyond the token range.
* @return {Ext.parse.Parser}
*/
reset: function(text, pos, end) {
var me = this;
me.error = me.token = null;
me.tokenizer.reset(text, pos, end);
me.advance(); // kick start this.token
return me;
},
/**
* This method is called when a syntax error is encountered. It updates `error`
* and returns the error token.
* @param {Number} at The index of the syntax error (optional).
* @param {String} message The error message.
* @return {Object} The error token.
*/
syntaxError: function(at, message) {
if (typeof at === 'string') {
message = at;
at = this.pos;
}
// eslint-disable-next-line vars-on-top
var suffix = (at == null) ? '' : (' (at index ' + at + ')'),
error = new Error(message + suffix);
error.type = 'error';
if (suffix) {
error.at = at;
}
throw this.error = error;
},
privates: {
/**
* This property is set to an `Error` instance if the parser encounters a syntax
* error.
* @property {Object} error
* @readonly
*/
error: null,
addSymbol: function(id, config, type, update) {
var symbols = this.symbols,
symbol = symbols[id],
cfg, length, i;
if (symbol) {
// If the symbol was already defined then we need to update it
// we either use the config provided in the symbol definition
// or we use the `update` param to build a config object.
// We usually need to update either `led` or `nud` function
if (typeof config === 'object') {
cfg = config;
}
else if (update && type) {
update = Ext.Array.from(update);
length = update.length;
cfg = {};
for (i = 0; i < length; i++) {
cfg[update[i]] = type.prototype[update[i]];
}
}
else {
return symbol;
}
symbol.update(cfg);
}
else {
if (config && config.xclass) {
type = Ext.ClassManager.get(config.xclass);
}
else {
type = type || Ext.parse.Symbol;
}
symbols[id] = symbol = new type(id, config);
symbol.parser = this;
}
return symbol;
},
addSymbols: function(symbols, type, update) {
var id;
for (id in symbols) {
this.addSymbol(id, symbols[id], type, update);
}
},
applyConstants: function(constants) {
this.addSymbols(constants, Ext.parse.symbol.Constant, 'nud');
},
applyInfix: function(operators) {
this.addSymbols(operators, Ext.parse.symbol.Infix, 'led');
},
applyInfixRight: function(operators) {
this.addSymbols(operators, Ext.parse.symbol.InfixRight, 'led');
},
applyPrefix: function(operators) {
this.addSymbols(operators, Ext.parse.symbol.Prefix, 'nud');
},
applySymbols: function(symbols) {
this.addSymbols(symbols);
},
applyTokenizer: function(config) {
var ret = config;
if (config && !config.isTokenizer) {
ret = new Ext.parse.Tokenizer(config);
}
this.tokenizer = ret;
}
}
};
});