forked from enviPath/enviPy
439 lines
14 KiB
JavaScript
Executable File
439 lines
14 KiB
JavaScript
Executable File
/*
|
|
* Copyright (C) 2012-2013 Nicolas Froidure
|
|
*
|
|
* This file is free software;
|
|
* you can redistribute it and/or modify it under the terms of the GNU
|
|
* General Public License (GPL) as published by the Free Software
|
|
* Foundation, in version 3. It is distributed in the
|
|
* hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
|
|
*
|
|
*/
|
|
|
|
// AMD + global + NodeJS : You can use this object by inserting a script
|
|
// or using an AMD loader (like RequireJS) or using NodeJS
|
|
(function(root,define){ define([], function() {
|
|
'use strict';
|
|
// START: Module logic start
|
|
|
|
// Constructor
|
|
function VarStreamReader (scope, prop, options) {
|
|
// Keep a ref to the root scope
|
|
this.rootScope={root: scope, prop: prop};
|
|
// Save the options
|
|
this.options=options;
|
|
// Store current scopes for backward references
|
|
this.previousNodes=[];
|
|
// The parse state
|
|
this.state=PARSE_NEWLINE;
|
|
// The current values
|
|
this.leftValue='';
|
|
this.rightValue='';
|
|
this.operator='';
|
|
this.escaped=ESC_NONE;
|
|
}
|
|
|
|
// Static consts
|
|
VarStreamReader.STRICT_MODE=1;
|
|
VarStreamReader.OPTIONS=VarStreamReader.STRICT_MODE;
|
|
|
|
// Constants
|
|
// Chars
|
|
var CHR_ENDL = '\n'
|
|
, CHR_CR = '\r'
|
|
, CHR_SEP = '.'
|
|
, CHR_BCK = '^'
|
|
, CHR_EQ = '='
|
|
, CHR_ESC = '\\'
|
|
, CHR_PLU = '+'
|
|
, CHR_MIN = '-'
|
|
, CHR_MUL = '*'
|
|
, CHR_DIV = '/'
|
|
, CHR_MOD = '%'
|
|
, CHR_REF = '&'
|
|
, CHR_NEW = '!'
|
|
, CHR_COM = '#'
|
|
// Chars sets
|
|
, EQ_OPS = [CHR_PLU,CHR_MIN,CHR_MUL,CHR_DIV,CHR_MOD,CHR_REF]
|
|
, ARRAY_OPS = [CHR_PLU,CHR_MUL,CHR_NEW]
|
|
, ARRAY_NODE_CHARS = /^[0-9]+$/
|
|
, PROP_NODE_CHARS = /^[a-zA-Z0-9_]+$/
|
|
, BCK_CHARS = /^\^[0-9]*$/
|
|
// Parsing status
|
|
, PARSE_NEWLINE = 1
|
|
, PARSE_LVAL = 2
|
|
, PARSE_OPERATOR = 3
|
|
, PARSE_RVAL = 4
|
|
, PARSE_MLSTRING = 5
|
|
, PARSE_COMMENT = 6
|
|
, PARSE_SILENT = 7
|
|
// Escape status
|
|
, ESC_NONE = 0
|
|
, ESC_LF = 1
|
|
, ESC_ALL = 3
|
|
;
|
|
|
|
VarStreamReader.prototype.resolveScope = function (val) {
|
|
var nodes = val.split(CHR_SEP)
|
|
, scope = this.rootScope
|
|
, n = 0
|
|
;
|
|
|
|
// Looking for backward refs in the first node
|
|
if(nodes[0] && nodes[0][0] == CHR_BCK) {
|
|
// if no numbers adding every previous nodes
|
|
if(nodes[0] == CHR_BCK) {
|
|
n = this.previousNodes.length ? this.previousNodes.length - 1 : 0;
|
|
// if numbers
|
|
} else {
|
|
// check it
|
|
if(!BCK_CHARS.test(nodes[0])) {
|
|
if(this.options&VarStreamReader.STRICT_MODE) {
|
|
throw new Error('Malformed backward reference.');
|
|
}
|
|
return null;
|
|
}
|
|
n = parseInt(nodes[0].substring(1), 10);
|
|
}
|
|
if(n > this.previousNodes.length) {
|
|
if(this.options&VarStreamReader.STRICT_MODE) {
|
|
throw new SyntaxError('Backward reference index is greater than the'
|
|
+ ' previous node max index.');
|
|
}
|
|
return null;
|
|
}
|
|
this.previousNodes.length = n;
|
|
nodes.shift();
|
|
nodes.unshift.apply(nodes,this.previousNodes);
|
|
}
|
|
|
|
// Looping throught each nodes
|
|
for(var i=0, j=nodes.length; i<j; i++) {
|
|
// Checking if the node is not empty
|
|
if(''===nodes[i]) {
|
|
if(this.options&VarStreamReader.STRICT_MODE) {
|
|
throw new Error('The leftValue can\'t have empty nodes ('+val+').');
|
|
}
|
|
return null;
|
|
}
|
|
// Array operators
|
|
if(-1!==ARRAY_OPS.indexOf(nodes[i])||ARRAY_NODE_CHARS.test(nodes[i])) {
|
|
// Ensure the scope is an array
|
|
if('undefined'=== typeof scope.root[scope.prop]
|
|
||!(scope.root[scope.prop] instanceof Array)) {
|
|
scope.root[scope.prop]=[];
|
|
}
|
|
if(nodes[i]===CHR_PLU) {
|
|
nodes[i]=scope.root[scope.prop].length;
|
|
}
|
|
if(nodes[i]===CHR_MUL) {
|
|
nodes[i]=scope.root[scope.prop].length-1;
|
|
}
|
|
if(nodes[i]===CHR_NEW) {
|
|
nodes[i]=scope.root[scope.prop].length=0;
|
|
}
|
|
} else {
|
|
// Checking node chars
|
|
if(!PROP_NODE_CHARS.test(nodes[i])) {
|
|
if(this.options&VarStreamReader.STRICT_MODE) {
|
|
throw new SyntaxError('Illegal chars found in a the node'
|
|
+ ' "'+nodes[i]+'".');
|
|
}
|
|
return null;
|
|
}
|
|
// Ensure the scope is an object
|
|
if('undefined'=== typeof scope.root[scope.prop]
|
|
||!(scope.root[scope.prop] instanceof Object)) {
|
|
scope.root[scope.prop]={};
|
|
}
|
|
}
|
|
// Resolving the node scope
|
|
scope={
|
|
root : scope.root[scope.prop],
|
|
prop : nodes[i]
|
|
};
|
|
}
|
|
|
|
// Keep previous nodes for backwards references
|
|
this.previousNodes = nodes;
|
|
|
|
return scope;
|
|
};
|
|
|
|
VarStreamReader.prototype.read = function (chunk) {
|
|
// Looping throught chunk chars
|
|
for(var i=0, j=chunk.length; i<j; i++) {
|
|
// detect escaped chars
|
|
if(chunk[i]===CHR_ESC && (
|
|
this.state===PARSE_RVAL
|
|
||this.state===PARSE_SILENT
|
|
||this.state===PARSE_MLSTRING
|
|
)
|
|
) {
|
|
if(this.escaped) {
|
|
this.escaped=ESC_NONE;
|
|
} else {
|
|
this.escaped=ESC_ALL;
|
|
continue;
|
|
}
|
|
}
|
|
// parsing chars according to the current state
|
|
switch(this.state) {
|
|
// Continue while newlines
|
|
case PARSE_NEWLINE:
|
|
this.escaped=ESC_NONE;
|
|
this.operator='';
|
|
this.leftValue='';
|
|
this.rightValue='';
|
|
if(chunk[i]===CHR_ENDL||chunk[i]===CHR_CR) {
|
|
continue;
|
|
}
|
|
// Read left value content
|
|
case PARSE_LVAL:
|
|
// Detect comments
|
|
if(chunk[i]===CHR_COM) {
|
|
this.state=PARSE_COMMENT;
|
|
continue;
|
|
}
|
|
// Detect special operators
|
|
if(this.leftValue.lastIndexOf(CHR_SEP)!=this.leftValue.length-1
|
|
&&-1!==EQ_OPS.indexOf(chunk[i])) {
|
|
this.state=PARSE_OPERATOR;
|
|
this.operator=chunk[i];
|
|
continue;
|
|
}
|
|
// Detect the = operator
|
|
if(CHR_EQ===chunk[i]) {
|
|
this.state=PARSE_RVAL;
|
|
this.operator=chunk[i];
|
|
continue;
|
|
}
|
|
// Fail if a new line is found
|
|
if(chunk[i]===CHR_ENDL||chunk[i]===CHR_CR) {
|
|
if(this.options&VarStreamReader.STRICT_MODE) {
|
|
throw SyntaxError('Unexpected new line found while parsing '
|
|
+' a leftValue.');
|
|
}
|
|
this.state=PARSE_NEWLINE;
|
|
continue;
|
|
}
|
|
// Store LVAL chars
|
|
this.state=PARSE_LVAL;
|
|
this.leftValue+=chunk[i];
|
|
continue;
|
|
// Read right value content
|
|
case PARSE_RVAL:
|
|
// Left value should not be empty
|
|
if(''===this.leftValue) {
|
|
if(this.options&VarStreamReader.STRICT_MODE) {
|
|
throw SyntaxError('Found an empty leftValue.');
|
|
}
|
|
this.state=PARSE_SILENT;
|
|
}
|
|
// Stop if a new line is found
|
|
if(chunk[i]===CHR_ENDL||chunk[i]===CHR_CR) {
|
|
// rightValue can be empty only with the = operator
|
|
// or if the string is multiline
|
|
if(this.operator!=CHR_EQ&&''===this.rightValue
|
|
&&this.escaped===ESC_NONE) {
|
|
if(this.options&VarStreamReader.STRICT_MODE) {
|
|
throw SyntaxError('Found an empty rightValue.');
|
|
}
|
|
this.state=PARSE_NEWLINE;
|
|
continue;
|
|
}
|
|
// Compute rval
|
|
// if it's a ref
|
|
if(this.operator===CHR_REF) {
|
|
this.rightValue=this.resolveScope(this.rightValue);
|
|
} else if('null'===this.rightValue) {
|
|
this.rightValue=null;
|
|
// Booleans
|
|
} else if('true'===this.rightValue) {
|
|
this.rightValue=true;
|
|
} else if('false'===this.rightValue) {
|
|
this.rightValue=false;
|
|
// Numbers
|
|
} else if('NaN'===this.rightValue) {
|
|
this.rightValue=NaN;
|
|
} else if(/^\-?([0-9]+(\.[0-9]+)?|Infinity)$/
|
|
.test(this.rightValue)) {
|
|
this.rightValue=Number(this.rightValue);
|
|
}
|
|
// Compute lval
|
|
this.leftValue=this.resolveScope(this.leftValue);
|
|
// set rval in lval (with operators)
|
|
if(null!==this.leftValue) {
|
|
switch(this.operator) {
|
|
case CHR_REF:
|
|
this.leftValue.root[this.leftValue.prop] = this.rightValue ?
|
|
this.rightValue.root[this.rightValue.prop] :
|
|
null;
|
|
break;
|
|
case CHR_EQ:
|
|
if(this.rightValue!=='' || 'string' ===
|
|
typeof this.leftValue.root[this.leftValue.prop]) {
|
|
this.leftValue.root[this.leftValue.prop]=this.rightValue;
|
|
} else {
|
|
delete this.leftValue.root[this.leftValue.prop];
|
|
}
|
|
break;
|
|
case CHR_PLU:
|
|
this.leftValue.root[this.leftValue.prop] +=
|
|
null === this.rightValue ? NaN : this.rightValue;
|
|
break;
|
|
case CHR_MIN:
|
|
this.leftValue.root[this.leftValue.prop] -=
|
|
null === this.rightValue ? NaN : this.rightValue;
|
|
break;
|
|
case CHR_MUL:
|
|
this.leftValue.root[this.leftValue.prop] *=
|
|
null === this.rightValue ? NaN : this.rightValue;
|
|
break;
|
|
case CHR_DIV:
|
|
this.leftValue.root[this.leftValue.prop] /=
|
|
null === this.rightValue ? NaN : this.rightValue;
|
|
break;
|
|
case CHR_MOD:
|
|
this.leftValue.root[this.leftValue.prop] %=
|
|
null === this.rightValue ? NaN : this.rightValue;
|
|
break;
|
|
}
|
|
}
|
|
// if the newline was escaped, continue to read the string
|
|
if(this.escaped) {
|
|
if(chunk[i]===CHR_CR) {
|
|
this.escaped=ESC_LF;
|
|
} else {
|
|
this.escaped=ESC_NONE;
|
|
}
|
|
if(null!==this.leftValue) {
|
|
this.state=PARSE_MLSTRING;
|
|
this.leftValue.root[this.leftValue.prop]+=chunk[i];
|
|
} else {
|
|
this.state=PARSE_SILENT;
|
|
}
|
|
} else {
|
|
this.state=PARSE_NEWLINE;
|
|
}
|
|
continue;
|
|
}
|
|
// Store RVAL chars
|
|
if(this.escaped) {
|
|
if(this.escaped==ESC_ALL) {
|
|
if(this.options&VarStreamReader.STRICT_MODE) {
|
|
throw Error('Found an escape char but there was nothing to escape.');
|
|
}
|
|
this.rightValue+='\\';
|
|
}
|
|
this.escaped=ESC_NONE;
|
|
}
|
|
this.rightValue+=chunk[i];
|
|
continue;
|
|
// Parse the content of a multiline value
|
|
case PARSE_MLSTRING:
|
|
if(this.escaped) {
|
|
if(this.escaped===ESC_ALL&&chunk[i]===CHR_CR) {
|
|
this.escaped=ESC_LF;
|
|
} else if(chunk[i]===CHR_ENDL) {
|
|
this.escaped=ESC_NONE;
|
|
} else {
|
|
if(this.escaped===ESC_LF) {
|
|
if(this.options&VarStreamReader.STRICT_MODE) {
|
|
throw new SyntaxError('Assuming a LF after an escaped CR, '
|
|
+chunk[i]+' found instead.');
|
|
}
|
|
} else {
|
|
if(this.options&VarStreamReader.STRICT_MODE) {
|
|
throw new SyntaxError('Found an escape char but there was'
|
|
+ ' nothing to escape.');
|
|
}
|
|
this.leftValue.root[this.leftValue.prop]+='\\';
|
|
}
|
|
this.escaped=ESC_NONE;
|
|
}
|
|
} else if(chunk[i]===CHR_ENDL||chunk[i]===CHR_CR) {
|
|
this.state=PARSE_NEWLINE;
|
|
continue;
|
|
}
|
|
// Store RVAL chars
|
|
this.leftValue.root[this.leftValue.prop]+=chunk[i];
|
|
continue;
|
|
// Finding the = char after an operator
|
|
case PARSE_OPERATOR:
|
|
if(chunk[i]===CHR_EQ) {
|
|
this.state=PARSE_RVAL;
|
|
continue;
|
|
}
|
|
if(this.options&VarStreamReader.STRICT_MODE) {
|
|
throw new SyntaxError('Unexpected char after the'
|
|
+ ' "'+this.operator+'" operator. Expected "="'
|
|
+ ' found "'+chunk[i]+'".');
|
|
}
|
|
if(chunk[i]===CHR_ENDL || chunk[i]===CHR_CR) {
|
|
this.state=PARSE_NEWLINE;
|
|
} else {
|
|
this.state=PARSE_SILENT;
|
|
}
|
|
continue;
|
|
// Parsing a comment content
|
|
case PARSE_COMMENT:
|
|
if((chunk[i]===CHR_ENDL&&!(this.escaped&ESC_LF))
|
|
||(chunk[i]===CHR_CR&&!(this.escaped&ESC_ALL))) {
|
|
this.state=PARSE_NEWLINE;
|
|
continue;
|
|
}
|
|
if(chunk[i]===CHR_CR&&(this.escaped&ESC_ALL)) {
|
|
this.escaped=ESC_LF;
|
|
}
|
|
if(chunk[i]===CHR_ENDL&&(this.escaped&ESC_LF)) {
|
|
this.escaped=ESC_NONE;
|
|
}
|
|
continue;
|
|
// Something was wrong, waiting for a newline to continue parsing
|
|
case PARSE_SILENT:
|
|
if((chunk[i]===CHR_ENDL&&!(this.escaped&ESC_LF))
|
|
||(chunk[i]===CHR_CR&&!(this.escaped&ESC_ALL))) {
|
|
this.state=PARSE_NEWLINE;
|
|
continue;
|
|
}
|
|
if(chunk[i]===CHR_CR&&(this.escaped&ESC_ALL)) {
|
|
this.escaped=ESC_LF;
|
|
}
|
|
if(chunk[i]===CHR_ENDL&&(this.escaped&ESC_LF)) {
|
|
this.escaped=ESC_NONE;
|
|
}
|
|
continue;
|
|
}
|
|
}
|
|
};
|
|
|
|
// END: Module logic end
|
|
|
|
return VarStreamReader;
|
|
|
|
});})(this,typeof define === 'function' && define.amd ?
|
|
// AMD
|
|
define :
|
|
// NodeJS
|
|
(typeof exports === 'object'?function (name, deps, factory) {
|
|
var root=this;
|
|
if(typeof name === 'object') {
|
|
factory=deps; deps=name;
|
|
}
|
|
module.exports=factory.apply(this, deps.map(function(dep){
|
|
return require(dep);
|
|
}));
|
|
}:
|
|
// Global
|
|
function (name, deps, factory) {
|
|
var root=this;
|
|
if(typeof name === 'object') {
|
|
factory=deps; deps=name;
|
|
}
|
|
this.VarStreamReader=factory.apply(this, deps.map(function(dep){
|
|
return root[dep.substring(dep.lastIndexOf('/')+1)];
|
|
}));
|
|
}.bind(this)
|
|
)
|
|
);
|