Comparing version 1.0.0-alpha.6 to 1.0.0-alpha.7
@@ -0,1 +1,11 @@ | ||
## 1.0.0-alpha.7 | ||
- Disallowed a whitespace between a dot and a bracket in `.[`, `.(` and `..(` tokens | ||
- Changed filter (i.e. `.[]` or `.filter()`) behaviour for a non-array value to return a value itself when expression is truthy or `undefined` otherwise | ||
- Changed semanthic `jora(query, methods?, debug?)` -> `jora(query, options?)` where `options` is `{ methods?, debug? }` | ||
- Added stat mode (turns on by `stat` option, i.e. `jora(query, { stat: true })`) to return a query stat interface (an object with `stat()` and `suggestion()` methods) instead of resulting data | ||
- Added tolerant parse mode (turns on by `tolerant` option, i.e. `jora(query, { tolerant: true })`) to supress parsing errors when possible | ||
- Added library `version` to export | ||
- Fixed parser edge cases for division (`/`) and regexp | ||
## 1.0.0-alpha.6 (December 7, 2018) | ||
@@ -2,0 +12,0 @@ |
{ | ||
"name": "jora", | ||
"version": "1.0.0-alpha.6", | ||
"version": "1.0.0-alpha.7", | ||
"description": "JavaScript object query engine", | ||
@@ -28,4 +28,4 @@ "keywords": [], | ||
"test": "mocha --reporter progress", | ||
"build": "npm run build:parser && browserify --standalone jora src/index.js > dist/jora.js && uglifyjs dist/jora.js --compress --mangle -o dist/jora.min.js", | ||
"build:parser": "node -e \"console.log(require('./src/parser').generateModule() + ';module.exports=parser;');\" > dist/parser.js", | ||
"build": "npm run build:parser && browserify -t package-json-versionify --standalone jora src/index.js > dist/jora.js && uglifyjs dist/jora.js --compress --mangle -o dist/jora.min.js", | ||
"build:parser": "node -e \"var { strict, tolerant } = require('./src/parser');console.log(strict.generateModule({ moduleName: 'strictParser' }) + '\\x0a' + tolerant.generateModule({ moduleName: 'tolerantParser' }) + '\\x0amodule.exports = strictParser;\\x0astrictParser.strict = strictParser;\\x0astrictParser.tolerant = tolerantParser;');\" > dist/parser.js", | ||
"prepublishOnly": "npm run build", | ||
@@ -45,2 +45,3 @@ "coverage": "istanbul cover _mocha -- -R min", | ||
"mocha": "^5.2.0", | ||
"package-json-versionify": "^1.0.4", | ||
"uglify-es": "^3.3.9" | ||
@@ -47,0 +48,0 @@ }, |
@@ -20,3 +20,5 @@ # Jora | ||
const query = jora('foo.myMethod()', { | ||
myMethod(current) { /* do something and return a new value */ } | ||
methods: { | ||
myMethod(current) { /* do something and return a new value */ } | ||
} | ||
}); | ||
@@ -28,2 +30,32 @@ | ||
Options: | ||
- methods | ||
Type: `Object` | ||
Default: `undefined` | ||
Additional methods for using in query passed as an object, where a key is a method name and a value is a function to perform an action. It can override build-in methods. | ||
- debug | ||
Type: `Boolean` | ||
Default: `false` | ||
Enables debug output. | ||
- tolerant | ||
Type: `Boolean` | ||
Default: `false` | ||
Enables tolerant parsing mode. This mode supresses parsing errors when possible. | ||
- stat | ||
Type: `Boolean` | ||
Default: `false` | ||
Enables stat mode. When mode is enabled a query stat interface is returning instead of resulting data. | ||
## Syntax | ||
@@ -36,3 +68,3 @@ | ||
42<br>4.222<br>-12.34e56 | Numbers | ||
"string"<br>'string' | A string | ||
"string"<br>'string' | Strings | ||
/regexp/<br>/regexp/i | A JavaScript regexp, only `i` flag supported | ||
@@ -42,4 +74,12 @@ { } | Object initializer/literal syntax. You can use spread operator `...`, e.g. `{ a: 1, ..., ...foo, ...bar }` (`...` with no expression on right side the same as `...$`) | ||
< > | A function<br>NOTE: Syntax will be changed | ||
symbol<br>'sym \'bol!' | A sequence of chars that matches to `[a-zA-Z_][a-zA-Z_$0-9]*`, otherwise it must be enclosed in quotes | ||
### Keywords | ||
The follow keyword can be used as in JavaScript: | ||
- `true` | ||
- `false` | ||
- `null` | ||
- `undefined` | ||
### Comparisons | ||
@@ -66,3 +106,3 @@ | ||
not x | Boolean not (a `!` in JS) | ||
x ? y : z | If boolean x, value y, else z | ||
x ? y : z | If `x` is truthy than return `y` else return `z` | ||
( x ) | Explicity operator precedence | ||
@@ -72,3 +112,3 @@ | ||
jora | Description | ||
Jora | Description | ||
--- | --- | ||
@@ -81,16 +121,42 @@ x + y | Add | ||
### Queries | ||
### Special variables | ||
jora | Description | ||
Jora | Description | ||
--- | --- | ||
@ | The root data object | ||
$ | The current data object | ||
$ | The current data object, depends on scope | ||
\# | The context | ||
### A block | ||
A block contains of a definition list (should comes first) and an expression. Both are optional. When an expression is empty a current value (i.e. `$`) returns. | ||
The syntax of definition (white spaces between any part are optional): | ||
``` | ||
$ SYMBOL ; | ||
$ SYMBOL : expression ; | ||
``` | ||
For example: | ||
``` | ||
$foo:123; // Define `$foo` variable | ||
$bar; // The same as `$bar:$.bar;` or `$a:bar;` | ||
$baz: $foo + $bar; // Variables can be used inside an expression after its definition | ||
``` | ||
A block creates a new scope. Variables can't be redefined in the same and nested scopes, otherwise it cause to error. | ||
### Path chaining | ||
jora | Description | ||
--- | --- | ||
SYMBOL | The same as `$.SYMBOL` | ||
.e | Child member operator (example: `foo.bar.baz`, `#.foo.'use any symbols for name'`) | ||
..e | Recursive descendant operator (example: `..deps`, `..(deps + dependants)`) | ||
.[ e ] | Filter a current data. Equivalent to a `.filter(<e>)` | ||
.( e ) | Map a current data. Equivalent to a `.map(<e>)` | ||
.SYMBOL | Child member operator (example: `foo.bar.baz`, `#.foo['use any symbols for name']`) | ||
..SYMBOL<br> ..( block ) | Recursive descendant operator (example: `..deps`, `..(deps + dependants)`) | ||
.[ block ] | Filter a current data. Equivalent to a `.filter(<block>)` | ||
.( block ) | Map a current data. Equivalent to a `.map(<block>)` | ||
.method() | Invoke a method to current data, or each element of current data if it is an array | ||
path[key] | Array-like notation to access properties. It works like in JS for everything with exception for arrays, where it equivalents to `array.map(e => e[key])`. Use `pick()` method to get an element by index in array. | ||
path[e] | Array-like notation to access properties. It works like in JS for everything with exception for arrays, where it equivalents to `array.map(e => e[key])`. Use `pick()` method to get an element by index in array. | ||
@@ -106,7 +172,7 @@ ## Build-in methods | ||
mapToArray("key"[, "value"]) | Converts an object to an array, and store object key as "key" | ||
pick("key") | Get a value by a key or an index. Useful for arrays, e.g. since `array[5]` applies `[5]` for each element in an array (equivalent to `array.map(e => e[5])`), `array.pick(5)` should be used instead. | ||
pick("key")<br>pick(fn) | Get a value by a key, an index or a function. Useful for arrays, e.g. since `array[5]` applies `[5]` for each element in an array (equivalent to `array.map(e => e[5])`), `array.pick(5)` should be used instead. | ||
size() | Returns count of keys if current data is object, otherwise returns `length` value or `0` when field is absent | ||
sort(\<getter>) | Sort an array by a value fetched with getter | ||
sort(\<fn>) | Sort an array by a value fetched with getter | ||
reverse() | Reverse order of items | ||
group(\<getter>[, \<getter>]) | Group an array items by a value fetched with first getter. | ||
group(\<fn>[, \<fn>]) | Group an array items by a value fetched with first getter. | ||
filter(\<fn>) | The same as `Array#filter()` in JS | ||
@@ -113,0 +179,0 @@ map(\<fn>) | The same as `Array#map()` in JS |
569
src/index.js
@@ -1,397 +0,310 @@ | ||
const parser = require('./parser'); | ||
const hasOwnProperty = Object.prototype.hasOwnProperty; | ||
const cache = Object.create(null); | ||
const buildin = require('./buildin'); | ||
const methods = require('./methods'); | ||
const { | ||
strict: strictParser, | ||
tolerant: tolerantParser | ||
} = require('./parser'); | ||
const { addToSet, isPlainObject} = require('./utils'); | ||
const TYPE_ARRAY = 1; | ||
const TYPE_OBJECT = 2; | ||
const TYPE_SCALAR = 3; | ||
const cacheStrict = new Map(); | ||
const cacheStrictStat = new Map(); | ||
const cacheTollerant = new Map(); | ||
const cacheTollerantStat = new Map(); | ||
const contextToType = { | ||
'path': 'property', | ||
'key': 'property', | ||
'value': 'value', | ||
'in-value': 'value', | ||
'var': 'variable' | ||
}; | ||
function noop() {} | ||
function self(value) { | ||
return value; | ||
function isWhiteSpace(str, offset) { | ||
const code = str.charCodeAt(offset); | ||
return code === 9 || code === 10 || code === 13 || code === 32; | ||
} | ||
function getPropertyValue(value, property) { | ||
return value && hasOwnProperty.call(value, property) ? value[property] : undefined; | ||
} | ||
function isPlainObject(value) { | ||
return value && typeof value === 'object' && value.constructor === Object; | ||
} | ||
function addToSet(value, set) { | ||
if (value !== undefined) { | ||
if (Array.isArray(value)) { | ||
value.forEach(item => set.add(item)); | ||
} else { | ||
set.add(value); | ||
function valuesToSuggestions(context, values) { | ||
const suggestions = new Set(); | ||
const addValue = value => { | ||
switch (typeof value) { | ||
case 'string': | ||
suggestions.add(JSON.stringify(value)); | ||
break; | ||
case 'number': | ||
suggestions.add(String(value)); | ||
break; | ||
} | ||
} | ||
} | ||
}; | ||
var buildin = Object.freeze({ | ||
type: function(value) { | ||
if (Array.isArray(value)) { | ||
return TYPE_ARRAY; | ||
} | ||
switch (context) { | ||
case '': | ||
case 'path': | ||
values.forEach(value => { | ||
if (Array.isArray(value)) { | ||
value.forEach(item => { | ||
if (isPlainObject(item)) { | ||
addToSet(suggestions, Object.keys(item)); | ||
} | ||
}); | ||
} else if (isPlainObject(value)) { | ||
addToSet(suggestions, Object.keys(value)); | ||
} | ||
}); | ||
break; | ||
if (isPlainObject(value)) { | ||
return TYPE_OBJECT; | ||
} | ||
case 'key': | ||
values.forEach(value => { | ||
if (isPlainObject(value)) { | ||
addToSet(suggestions, Object.keys(value)); | ||
} | ||
}); | ||
break; | ||
return TYPE_SCALAR; | ||
}, | ||
bool: function(data) { | ||
switch (this.type(data)) { | ||
case TYPE_ARRAY: | ||
return data.length > 0; | ||
case TYPE_OBJECT: | ||
for (let key in data) { | ||
if (hasOwnProperty.call(data, key)) { | ||
return true; | ||
} | ||
case 'value': | ||
values.forEach(value => { | ||
if (Array.isArray(value)) { | ||
value.forEach(addValue); | ||
} else { | ||
addValue(value); | ||
} | ||
return false; | ||
}); | ||
break; | ||
default: | ||
return Boolean(data); | ||
} | ||
}, | ||
add: function(a, b) { | ||
const typeA = this.type(a); | ||
const typeB = this.type(b); | ||
if (typeA !== TYPE_ARRAY) { | ||
if (typeB === TYPE_ARRAY) { | ||
[a, b] = [b, a]; | ||
} | ||
} | ||
switch (this.type(a)) { | ||
case TYPE_ARRAY: | ||
return [...new Set([].concat(a, b))]; | ||
case TYPE_OBJECT: | ||
return Object.assign({}, a, b); | ||
default: | ||
return a + b; | ||
} | ||
}, | ||
sub: function(a, b) { | ||
switch (this.type(a)) { | ||
case TYPE_ARRAY: | ||
const result = new Set(a); | ||
// filter b items from a | ||
if (Array.isArray(b)) { | ||
b.forEach(item => result.delete(item)); | ||
case 'in-value': | ||
values.forEach(value => { | ||
if (Array.isArray(value)) { | ||
value.forEach(addValue); | ||
} else if (isPlainObject(value)) { | ||
Object.keys(value).forEach(addValue); | ||
} else { | ||
result.delete(b); | ||
addValue(value); | ||
} | ||
}); | ||
break; | ||
return [...result]; | ||
case 'var': | ||
values.forEach(value => { | ||
suggestions.add('$' + value); | ||
}); | ||
break; | ||
} | ||
case TYPE_OBJECT: | ||
// not sure what we need do here: | ||
// - just filter keys from `a` | ||
// - or filter key+value pairs? | ||
// - take in account type of b? (array, Object.keys(b), scalar as a key) | ||
return [...suggestions]; | ||
} | ||
default: | ||
return a - b; | ||
} | ||
}, | ||
mul: function(a, b) { | ||
return a * b; | ||
}, | ||
div: function(a, b) { | ||
return a / b; | ||
}, | ||
mod: function(a, b) { | ||
return a % b; | ||
}, | ||
eq: function(a, b) { | ||
return a === b; | ||
}, | ||
ne: function(a, b) { | ||
return a !== b; | ||
}, | ||
lt: function(a, b) { | ||
return a < b; | ||
}, | ||
lte: function(a, b) { | ||
return a <= b; | ||
}, | ||
gt: function(a, b) { | ||
return a > b; | ||
}, | ||
gte: function(a, b) { | ||
return a >= b; | ||
}, | ||
in: function(a, b) { | ||
switch (this.type(b)) { | ||
case TYPE_OBJECT: | ||
return hasOwnProperty.call(b, a); | ||
function findSourcePosPoints(source, pos, points, includeEmpty) { | ||
const result = []; | ||
default: | ||
return b && typeof b.indexOf === 'function' ? b.indexOf(a) !== -1 : false; | ||
} | ||
}, | ||
regexp: function(data, rx) { | ||
switch (this.type(data)) { | ||
case TYPE_ARRAY: | ||
return this.filter(data, current => rx.test(current)); | ||
for (let i = 0; i < points.length; i++) { | ||
let [values, from, to, context] = points[i]; | ||
default: | ||
return rx.test(data); | ||
} | ||
}, | ||
get: function(data, getter) { | ||
const fn = typeof getter === 'function' | ||
? getter | ||
: current => getPropertyValue(current, getter); | ||
if (pos >= from && pos <= to && (includeEmpty || values.size || values.length)) { | ||
let current = source.substring(from, to); | ||
switch (this.type(data)) { | ||
case TYPE_ARRAY: | ||
const result = new Set(); | ||
if (!/\S/.test(current)) { | ||
current = ''; | ||
from = to = pos; | ||
} | ||
for (let i = 0; i < data.length; i++) { | ||
addToSet(fn(data[i]), result); | ||
} | ||
return [...result]; | ||
default: | ||
return data !== undefined ? fn(data) : data; | ||
result.push({ | ||
context, | ||
current, | ||
from, | ||
to, | ||
values | ||
}); | ||
} | ||
}, | ||
recursive: function(data, getter) { | ||
const result = new Set(); | ||
addToSet(this.get(data, getter), result); | ||
result.forEach(current => | ||
addToSet(this.get(current, getter), result) | ||
); | ||
return [...result]; | ||
}, | ||
filter: function(data, query) { | ||
switch (this.type(data)) { | ||
case TYPE_ARRAY: | ||
return data.filter(current => | ||
this.bool(query(current)) | ||
); | ||
default: | ||
return []; | ||
} | ||
} | ||
}); | ||
var methods = Object.freeze({ | ||
bool: function(current) { | ||
return buildin.bool(current); | ||
}, | ||
keys: function(current) { | ||
return Object.keys(current || {}); | ||
}, | ||
values: function(current) { | ||
const values = new Set(); | ||
return result; | ||
} | ||
Object | ||
.values(current || {}) | ||
.forEach(value => addToSet(value, values)); | ||
function compileFunction(source, statMode, tolerantMode, debug) { | ||
function astToCode(node, scopeVars) { | ||
if (Array.isArray(node)) { | ||
const first = node[0]; | ||
let varName = false; | ||
let i = 0; | ||
return [...values]; | ||
}, | ||
entries: function(current) { | ||
if (!current) { | ||
return []; | ||
} | ||
if (first === '/*scope*/') { | ||
// create new scope | ||
scopeVars = scopeVars.slice(); | ||
i++; | ||
} else if (typeof first === 'string' && first.startsWith('/*define:')) { | ||
let [from, to] = first.substring(9, first.length - 2).split(','); | ||
varName = source.substring(from, to); | ||
return Object | ||
.keys(current) | ||
.map(key => ({ key, value: current[key] })); | ||
}, | ||
pick: function(current, ref) { | ||
if (!current) { | ||
return undefined; | ||
} | ||
if (scopeVars.includes(`"${varName}"`)) { | ||
throw new Error(`Identifier '$${varName}' has already been declared`); | ||
} | ||
if (typeof ref === 'function') { | ||
if (Array.isArray(current)) { | ||
return current.find(item => ref(item)); | ||
i++; | ||
} | ||
for (const key in current) { | ||
if (hasOwnProperty.call(current, key)) { | ||
if (ref(current[key])) { | ||
return { key, value: current[key] }; | ||
} | ||
} | ||
for (; i < node.length; i++) { | ||
astToCode(node[i], scopeVars); | ||
} | ||
return; | ||
} | ||
return Array.isArray(current) ? current[ref || 0] : current[ref]; | ||
}, | ||
mapToArray: function(current, keyProperty = 'key', valueProperty) { | ||
const result = []; | ||
for (let key in current) { | ||
if (hasOwnProperty.call(current, key)) { | ||
result.push( | ||
valueProperty | ||
? { [keyProperty]: key, [valueProperty]: current[key] } | ||
: Object.assign({ [keyProperty]: key }, current[key]) | ||
); | ||
if (varName) { | ||
scopeVars.push(`"${varName}"`); | ||
} | ||
} | ||
} else if (statMode && node.startsWith('/*')) { | ||
if (node === '/*s*/') { | ||
code.push('suggestPoint('); | ||
} else if (node.startsWith('/*var:')) { | ||
let [from, to] = node.substring(6, node.length - 2).split(','); | ||
return result; | ||
}, | ||
size: function(current) { | ||
switch (buildin.type(current)) { | ||
case TYPE_ARRAY: | ||
return current.length; | ||
if (from === to) { | ||
while (to < source.length && isWhiteSpace(source, to)) { | ||
to++; | ||
} | ||
} | ||
case TYPE_OBJECT: | ||
return Object.keys(current).length; | ||
suggestPoints.push(`[[${scopeVars}], ${from}, ${to}, "var"]`); | ||
} else { | ||
const pointId = suggestSets.push('sp' + suggestSets.length + ' = new Set()') - 1; | ||
const items = node.substring(2, node.length - 2).split(','); | ||
default: | ||
return (current && current.length) || 0; | ||
} | ||
}, | ||
sort: function(current, fn) { | ||
if (buildin.type(current) !== TYPE_ARRAY) { | ||
return current; | ||
} | ||
// FIXME: position correction should be in parser | ||
for (let i = 0; i < items.length; i += 3) { | ||
let from = Number(items[i]); | ||
let to = Number(items[i + 1]); | ||
let context = items[i + 2]; | ||
const frag = source.substring(from, to); | ||
if (typeof fn === 'function') { | ||
return current.slice().sort((a, b) => { | ||
a = fn(a); | ||
b = fn(b); | ||
if (Array.isArray(a) && Array.isArray(b)) { | ||
if (a.length !== b.length) { | ||
return a.length < b.length ? -1 : 1; | ||
} | ||
for (let i = 0; i < a.length; i++) { | ||
if (a[i] < b[i]) { | ||
return -1; | ||
} else if (a[i] > b[i]) { | ||
return 1; | ||
if (frag === '.[' || frag === '.(' || frag === '..(' || | ||
frag === '{' || frag === '[' || frag === '(' || | ||
from === to) { | ||
from = to; | ||
while (to < source.length && isWhiteSpace(source, to)) { | ||
to++; | ||
} | ||
} | ||
return 0; | ||
suggestPoints.push(`[sp${pointId}, ${from}, ${to}, "${context || 'path'}"]`); | ||
} | ||
return a < b ? -1 : a > b; | ||
}); | ||
code.push(`, sp${pointId})`); | ||
} | ||
} else { | ||
code.push(node); | ||
} | ||
} | ||
return current.slice().sort(); | ||
}, | ||
reverse: function(current) { | ||
if (buildin.type(current) !== TYPE_ARRAY) { | ||
return current; | ||
} | ||
const parser = tolerantMode ? tolerantParser : strictParser; | ||
const code = []; | ||
const suggestPoints = []; | ||
let suggestSets = []; | ||
let body = []; | ||
let tree; | ||
return current.slice().reverse(); | ||
}, | ||
group: function(current, keyGetter, valueGetter) { | ||
if (typeof keyGetter !== 'function') { | ||
keyGetter = noop; | ||
} | ||
if (debug) { | ||
console.log('\n== compile ======'); | ||
console.log('source:', source); | ||
} | ||
if (typeof valueGetter !== 'function') { | ||
valueGetter = self; | ||
} | ||
tree = parser.parse(source); | ||
if (buildin.type(current) !== TYPE_ARRAY) { | ||
current = [current]; | ||
} | ||
// if (debug) { | ||
// console.log('tree:', JSON.stringify(tree, null, 4)); | ||
// } | ||
const map = new Map(); | ||
const result = []; | ||
astToCode(tree, []); | ||
current.forEach(item => { | ||
const key = keyGetter(item); | ||
if (map.has(key)) { | ||
map.get(key).add(valueGetter(item)); | ||
} else { | ||
map.set(key, new Set([valueGetter(item)])); | ||
} | ||
}); | ||
map.forEach((value, key) => | ||
result.push({ key, value: [...value] }) | ||
if (suggestSets.length) { | ||
body.push( | ||
'const ' + suggestSets.join(', ') + ';', | ||
'const suggestPoint = (value, set) => set.add(value) && value;' | ||
); | ||
return result; | ||
}, | ||
filter: function(current, fn) { | ||
return buildin.filter(current, fn); | ||
}, | ||
map: function(current, fn) { | ||
return buildin.get(current, fn); | ||
} | ||
}); | ||
function compileFunction(expression, debug) { | ||
var tree = parser.parse(expression); | ||
var js = []; | ||
body.push( | ||
// preserved variables | ||
'const $data = undefined, $context = undefined, $ctx = undefined, $array = undefined, $idx = undefined, $index = undefined;', | ||
'let current = data;', | ||
code.join('') | ||
); | ||
if (debug) { | ||
console.log('\n==== compile ==='); | ||
console.log('expression:', expression); | ||
console.log('tree:', tree); | ||
if (statMode) { | ||
body.push(`,[${suggestPoints}]`); | ||
} | ||
tree.forEach(function toJs(node) { | ||
if (Array.isArray(node)) { | ||
node.forEach(toJs); | ||
} else { | ||
js.push(node); | ||
} | ||
}); | ||
if (debug) { | ||
console.log('js', js.join('')); | ||
console.log('== body =========\n' + body.join('\n') + '\n=================\n'); | ||
} | ||
return cache[expression] = new Function( | ||
'fn', 'method', 'data', 'context', 'self', | ||
[ | ||
'let current = data;', | ||
'const $data = undefined, $context = undefined, $ctx = undefined, $array = undefined, $idx = undefined, $index = undefined;', | ||
js.join('') | ||
].join('\n') | ||
); | ||
return new Function('fn', 'method', 'data', 'context', 'self', body.join('\n')); | ||
} | ||
module.exports = function createQuery(expression, extraFunctions, debug) { | ||
expression = String(expression).trim(); | ||
module.exports = function createQuery(source, options) { | ||
options = options || {}; | ||
var localMethods = extraFunctions ? Object.assign({}, methods, extraFunctions) : methods; | ||
var func = cache[expression] || compileFunction(expression, debug); | ||
const debug = Boolean(options.debug); | ||
const statMode = Boolean(options.stat); | ||
const tolerantMode = Boolean(options.tolerant); | ||
const localMethods = options.methods ? Object.assign({}, methods, options.methods) : methods; | ||
const cache = statMode | ||
? (tolerantMode ? cacheTollerantStat : cacheStrictStat) | ||
: (tolerantMode ? cacheTollerant : cacheStrict); | ||
let fn; | ||
source = String(source); | ||
if (cache.has(source)) { | ||
fn = cache.get(source); | ||
} else { | ||
fn = compileFunction(source, statMode, tolerantMode, debug); | ||
cache.set(source, fn); | ||
} | ||
if (debug) { | ||
console.log('fn', func.toString()); | ||
console.log('fn', fn.toString()); | ||
} | ||
if (statMode) { | ||
return function query(data, context) { | ||
const points = fn(buildin, localMethods, data, context, query); | ||
return { | ||
stat(pos, includeEmpty) { | ||
const ranges = findSourcePosPoints(source, pos, points, includeEmpty); | ||
ranges.forEach(entry => { | ||
entry.values = [...entry.values]; | ||
}); | ||
return ranges.length ? ranges : null; | ||
}, | ||
suggestion(pos, includeEmpty) { | ||
const suggestions = []; | ||
findSourcePosPoints(source, pos, points, includeEmpty).forEach(entry => { | ||
const { context, current, from, to, values } = entry; | ||
// console.log({current, variants:[...suggestions.get(range)], suggestions }) | ||
suggestions.push( | ||
...valuesToSuggestions(context, values) | ||
.map(value => ({ | ||
current, | ||
type: contextToType[context], | ||
value, | ||
from, | ||
to | ||
})) | ||
); | ||
}); | ||
return suggestions.length ? suggestions : null; | ||
} | ||
}; | ||
}; | ||
} | ||
return function query(data, context) { | ||
return func(buildin, localMethods, data, context, query); | ||
return fn(buildin, localMethods, data, context, query); | ||
}; | ||
}; | ||
module.exports.version = require('../package.json').version; | ||
module.exports.buildin = buildin; | ||
module.exports.methods = methods; |
@@ -1,64 +0,125 @@ | ||
const Jison = require('jison'); | ||
const { Parser } = require('jison'); | ||
function code(s) { | ||
return '$$ = [' + | ||
s[0].split(/(\$\d+)/g).map((x, i) => i % 2 ? x : JSON.stringify(x)) + | ||
s[0].split(/(\$[\da-zA-Z_]+|\/\*(?:scope|define:\S+?|var:\S+?)\*\/|\/\*\S*@[\da-zA-Z_$]+(?:\/\S*@[\da-zA-Z_$]+)*\*\/\$?[\da-zA-Z_]+)/g).map( | ||
(m, i) => { | ||
if (i % 2 === 0 || m === '/*scope*/') { | ||
return JSON.stringify(m); | ||
} | ||
if (m.startsWith('/*define:')) { | ||
return '"/*define:" + ' + m.substring(9, m.length - 2) + '.range + "*/"'; | ||
} | ||
if (m.startsWith('/*var:')) { | ||
return '"/*var:" + ' + m.substring(6, m.length - 2) + '.range + "*/"'; | ||
} | ||
if (m.startsWith('/*')) { | ||
const content = m.substring(2, m.indexOf('*/')); | ||
const expr = m.substr(content.length + 4); | ||
const ranges = content.split('/').map(range => { | ||
const [context, loc] = range.split('@'); | ||
return '" + @' + loc + '.range + ",' + context; | ||
}); | ||
return ( | ||
'"/*s*/",' + | ||
(expr[0] === '$' ? expr : '"' + expr + '"') + | ||
',"/*' + ranges + '*/"' | ||
); | ||
} | ||
return m; | ||
} | ||
).filter(term => term !== '""') + | ||
'];'; | ||
} | ||
var grammar = { | ||
const switchToPreventRxState = 'if (this._input) this.begin("preventRx"); '; | ||
const grammar = { | ||
// Lexical tokens | ||
lex: { | ||
options: { | ||
ranges: true | ||
}, | ||
macros: { | ||
wb: '\\b', | ||
ows: '\\s*', // optional whitespaces | ||
ws: '\\s+' // required whitespaces | ||
ws: '\\s+', // required whitespaces | ||
comment: '//.*?(\\r|\\n|$)+', | ||
rx: '/(?:\\\\.|[^/])+/i?' | ||
}, | ||
startConditions: { | ||
preventRx: 0 | ||
}, | ||
rules: [ | ||
['\\({ows}', 'return "(";'], | ||
['{ows}\\)', 'return ")";'], | ||
['{ows}\\[{ows}', 'return "[";'], | ||
['{ows}\\]', 'return "]";'], | ||
['\\{{ows}', 'return "{";'], | ||
['{ows}\\}', 'return "}";'], | ||
// ignore comments and whitespaces | ||
['{comment}', '/* a comment */'], | ||
['{ws}', '/* a whitespace */'], | ||
['{ows}={ows}', 'return "=";'], | ||
['{ows}!={ows}', 'return "!=";'], | ||
['{ows}~={ows}', 'return "~=";'], | ||
['{ows}>={ows}', 'return ">=";'], | ||
['{ows}<={ows}', 'return "<=";'], | ||
['{ows}<{ows}', 'return "<";'], | ||
['{ows}>{ows}', 'return ">";'], | ||
['{ws}and{ws}', 'return "AND";'], | ||
['{ws}or{ws}' , 'return "OR";'], | ||
['{ws}in{ws}', 'return "IN";'], | ||
['{ws}not{ws}in{ws}', 'return "NOTIN";'], | ||
['not?{ws}', 'return "NOT";'], | ||
// hack to prevent regexp consumption | ||
[['preventRx'], '\\/', 'this.popState(); return "/";'], | ||
// FIXME: using `this.done = false;` is hack, since `regexp-lexer` set done=true | ||
// when no input left and doesn't take into account current state; | ||
// should be fixed in `regexp-lexer` | ||
[['preventRx'], '', 'this.done = false; this.popState();'], | ||
['{wb}true{wb}', 'return "TRUE";'], | ||
['{wb}false{wb}', 'return "FALSE";'], | ||
['{wb}null{wb}', 'return "NULL";'], | ||
['{wb}undefined{wb}', 'return "UNDEFINED";'], | ||
// braces | ||
['\\(', 'return "(";'], | ||
['\\)', switchToPreventRxState + 'return ")";'], | ||
['\\[', 'return "[";'], | ||
['\\]', switchToPreventRxState + 'return "]";'], | ||
['\\{', 'return "{";'], | ||
['\\}', 'return "}";'], | ||
// operators | ||
['=', 'return "=";'], | ||
['!=', 'return "!=";'], | ||
['~=', 'return "~=";'], | ||
['>=', 'return ">=";'], | ||
['<=', 'return "<=";'], | ||
['<', 'return "<";'], | ||
['>', 'return ">";'], | ||
['and{wb}', 'return "AND";'], | ||
['or{wb}' , 'return "OR";'], | ||
['in{wb}', 'return "IN";'], | ||
['not{ws}in{wb}', 'return "NOTIN";'], | ||
['not?{wb}', 'return "NOT";'], | ||
// keywords | ||
['true{wb}', 'return "TRUE";'], | ||
['false{wb}', 'return "FALSE";'], | ||
['null{wb}', 'return "NULL";'], | ||
['undefined{wb}', 'return "UNDEFINED";'], | ||
// self | ||
['::self', 'return "SELF";'], | ||
['[0-9]+(?:\\.[0-9]+)?\\b', 'return "NUMBER";'], // 212.321 | ||
['"(?:\\\\.|[^"])*"', 'return "STRING";'], // "foo" "with \" escaped" | ||
["'(?:\\\\.|[^'])*'", 'return "STRING";'], // 'foo' 'with \' escaped' | ||
['/(?:\\\\.|[^/])+/i?', 'return "REGEXP"'], // /foo/i | ||
['[a-zA-Z_][a-zA-Z_$0-9]*', 'return "SYMBOL";'], // foo123 | ||
// comment | ||
['{ows}//.*?(\\n|$)', '/* a comment */'], | ||
// primitives | ||
['\\d+(?:\\.\\d+)?{wb}', switchToPreventRxState + 'return "NUMBER";'], // 212.321 | ||
['"(?:\\\\.|[^"])*"', switchToPreventRxState + 'return "STRING";'], // "foo" "with \" escaped" | ||
["'(?:\\\\.|[^'])*'", switchToPreventRxState + 'return "STRING";'], // 'foo' 'with \' escaped' | ||
['{rx}', 'return "REGEXP"'], // /foo/i | ||
['[a-zA-Z_][a-zA-Z_$0-9]*', switchToPreventRxState + 'return "SYMBOL";'], // foo123 | ||
['{ows}\\.{1,3}', 'return yytext.trim();'], | ||
['{ows}\\?{ows}', 'return "?";'], | ||
['{ows},{ows}', 'return ",";'], | ||
['{ows}:{ows}', 'return ":";'], | ||
['{ows};{ows}', 'return ";";'], | ||
['{ows}\\-{ows}', 'return "-";'], | ||
['{ows}\\+{ows}', 'return "+";'], | ||
['{ows}\\*{ows}', 'return "*";'], | ||
['{ows}\\/{ows}', 'return "/";'], | ||
['{ows}\\%{ows}', 'return "%";'], | ||
// ['{ows}\\^{ows}', 'return "%";'], | ||
// operators | ||
['\\.\\.\\(', 'return "..(";'], | ||
['\\.\\(', 'return ".(";'], | ||
['\\.\\[', 'return ".[";'], | ||
['\\.\\.\\.', 'return "...";'], | ||
['\\.\\.', 'return "..";'], | ||
['\\.', 'return ".";'], | ||
['\\?', 'return "?";'], | ||
[',', 'return ",";'], | ||
[':', 'return ":";'], | ||
[';', 'return ";";'], | ||
['\\-', 'return "-";'], | ||
['\\+', 'return "+";'], | ||
['\\*', 'return "*";'], | ||
['\\/', 'return "/";'], | ||
['\\%', 'return "%";'], | ||
// special vars | ||
['@', 'return "@";'], | ||
@@ -85,4 +146,4 @@ ['#', 'return "#";'], | ||
['left', '*', '/', '%'], | ||
// ['left', '^'], | ||
['left', '.', '..', '...'] | ||
['left', '.', '..', '...'], | ||
['left', '.(', '.[', '..('] | ||
], | ||
@@ -97,6 +158,10 @@ // Grammar | ||
block: [ | ||
['nonEmptyBlock', code`/*scope*/$1`], | ||
['definitions', code`/*scope*/$1\nreturn current`], | ||
['', code`/*scope*/return /*@$*/current`] | ||
], | ||
nonEmptyBlock: [ | ||
['definitions e', code`$1\nreturn $2`], | ||
['definitions', code`$1\nreturn current`], | ||
['e', code`return $1`], | ||
['', code`return current`] | ||
['e', code`return $1`] | ||
], | ||
@@ -110,4 +175,4 @@ | ||
def: [ | ||
['$ SYMBOL ;', code`const $$2 = fn.get(current, "$2");`], | ||
['$ SYMBOL : e ;', code`const $$2 = $4;`] | ||
['$ SYMBOL ;', code`/*define:@2*/const $$2 = fn.get(/*key@2*/current, "$2");`], | ||
['$ SYMBOL : e ;', code`/*define:@2*/const $$2 = $4;`] | ||
], | ||
@@ -124,5 +189,8 @@ | ||
['function', code`$1`], | ||
['op', code`$1`] | ||
], | ||
op: [ | ||
['NOT e', code`!fn.bool($2)`], | ||
['e IN e', code`fn.in($1, $3)`], | ||
['e IN e', code`fn.in($1, /*in-value@1*/$3)`], | ||
['e NOTIN e', code`!fn.in($1, $3)`], | ||
@@ -137,4 +205,4 @@ ['e AND e', code`fn.bool($1) ? $3 : $1`], | ||
['e % e', code`fn.mod($1, $3)`], | ||
['e = e', code`fn.eq($1, $3)`], | ||
['e != e', code`fn.ne($1, $3)`], | ||
['e = e', code`fn.eq(/*value@3*/$1, $3)`], | ||
['e != e', code`fn.ne(/*value@3*/$1, $3)`], | ||
['e < e', code`fn.lt($1, $3)`], | ||
@@ -162,4 +230,4 @@ ['e <= e', code`fn.lte($1, $3)`], | ||
['#', code`context`], | ||
['$', code`current`], | ||
['$ SYMBOL', code`$$2`], | ||
['$', code`/*var:@1*/current`], | ||
['$ SYMBOL', code`/*var:@$*/typeof $$2 !== 'undefined' ? $$2 : undefined`], | ||
['STRING', code`$1`], | ||
@@ -170,20 +238,23 @@ ['NUMBER', code`$1`], | ||
['array', code`$1`], | ||
['SYMBOL', code`fn.get(current, "$1")`], | ||
['. SYMBOL', code`fn.get(current, "$2")`], | ||
['SYMBOL', code`/*var:@1*/fn.get(/*@1*/current, "$1", 'xxx')`], | ||
['. SYMBOL', code`fn.get(/*@2*/current, "$2")`], | ||
['( e )', code`($2)`], | ||
['. ( block )', code`fn.get(current, current => { $3 })`], | ||
['SYMBOL ( arguments )', code`method.$1(current$3)`], | ||
['. SYMBOL ( arguments )', code`method.$2(current$4)`], | ||
['.. SYMBOL', code`fn.recursive(current, "$2")`], | ||
['.. ( block )', code`fn.recursive(current, current => { $3 })`], | ||
['. [ block ]', code`fn.filter(current, current => { $3 })`] | ||
['.( block )', code`fn.get(current, current => { $2 })`], | ||
['SYMBOL ( )', code`method.$1(/*@1/@2*/current)`], | ||
['SYMBOL ( arguments )', code`method.$1(/*@1*/current, $3)`], | ||
['. SYMBOL ( )', code`method.$2(/*@2/@3*/current)`], | ||
['. SYMBOL ( arguments )', code`method.$2(/*@2*/current, $4)`], | ||
['.. SYMBOL', code`fn.recursive(/*@2*/current, "$2")`], | ||
['..( block )', code`fn.recursive(current, current => { $2 })`], | ||
['.[ block ]', code`fn.filter(current, current => { $2 })`] | ||
], | ||
relativePath: [ | ||
['query . SYMBOL', code`fn.get($1, "$3")`], | ||
['query . SYMBOL ( arguments )', code`method.$3($1$5)`], | ||
['query . ( block )', code`fn.get($1, current => { $4 })`], | ||
['query .. SYMBOL', code`fn.recursive($1, "$3")`], | ||
['query .. ( block )', code`fn.recursive($1, current => { $4 })`], | ||
['query . [ block ]', code`fn.filter($1, current => { $4 })`], | ||
['query . SYMBOL', code`fn.get(/*@3*/$1, "$3")`], | ||
['query . SYMBOL ( )', code`method.$3(/*@3/@4*/$1)`], | ||
['query . SYMBOL ( arguments )', code`method.$3(/*@3*/$1, $5)`], | ||
['query .( block )', code`fn.get($1, current => { $3 })`], | ||
['query .. SYMBOL', code`fn.recursive(/*@3*/$1, "$3")`], | ||
['query ..( block )', code`fn.recursive($1, current => { $3 })`], | ||
['query .[ block ]', code`fn.filter($1, current => { $3 })`], | ||
['query [ e ]', code`fn.get($1, $3)`] | ||
@@ -193,13 +264,8 @@ ], | ||
arguments: [ | ||
['', code``], | ||
['argumentList', code`$1`] | ||
['e', code`$1`], | ||
['arguments , e', code`$1, $3`] | ||
], | ||
argumentList: [ | ||
['e', code`, $1`], | ||
['argumentList , e', code`$1, $3`] | ||
], | ||
object: [ | ||
['{ }', code`({})`], | ||
['{ }', code`(/*@1*/current, {})`], | ||
['{ properties }', code`({ $2 })`] | ||
@@ -214,4 +280,4 @@ ], | ||
property: [ | ||
['SYMBOL', code`$1: fn.get(current, "$1")`], | ||
['$ SYMBOL', code`$2: $$2`], | ||
['SYMBOL', code`/*var:@1*/$1: fn.get(/*@1*/current, "$1")`], | ||
['$ SYMBOL', code`/*var:@$*/$2: typeof $$2 !== 'undefined' ? $$2 : undefined`], | ||
['SYMBOL : e', code`$1: $3`], | ||
@@ -224,5 +290,4 @@ ['[ e ] : e', code`[$2]: $5`], | ||
array: [ | ||
['[ ]', code`[]`], | ||
['[ e ]', code`[$2]`], | ||
['[ e , arrayItems ]', code`[$2, $4]`] | ||
['[ ]', code`(/*@1*/current, [])`], | ||
['[ arrayItems ]', code`[$2]`] | ||
], | ||
@@ -241,2 +306,90 @@ | ||
module.exports = new Jison.Parser(grammar); | ||
const tolerantScopeStart = new Set([ | ||
':', ';', | ||
'\\.', '\\.\\.', ',', | ||
'\\+', '\\-', '\\*', '\\/', '\\%', | ||
'=', '!=', '~=', '>=', '<=', /* '<', '>', */ | ||
'and{wb}', 'or{wb}', 'in{wb}', 'not{ws}in{wb}', 'not?{wb}' | ||
]); | ||
const tolerantGrammar = Object.assign({}, grammar, { | ||
lex: Object.assign({}, grammar.lex, { | ||
startConditions: Object.assign({}, grammar.lex.startConditions, { | ||
suggestPoint: 1, | ||
suggestPointWhenWhitespace: 1, | ||
suggestPointVar: 1, | ||
suggestPointVarDisabled: 0 | ||
}), | ||
rules: [ | ||
[['suggestPoint'], | ||
// prevent suggestions before rx | ||
'(?=({ows}{comment})*{ows}{rx})', | ||
'this.popState();' | ||
], | ||
[['suggestPoint'], | ||
'(?=({ows}{comment})*{ows}([\\]\\)\\}\\<\\>\\+\\-\\*\\/,~!=:;]|$))', | ||
// FIXME: using `this.done = false;` is hack, since `regexp-lexer` set done=true | ||
// when no input left and doesn't take into account current state; | ||
// should be fixed in `regexp-lexer` | ||
'this.popState(); this.done = false; yytext = "_"; ' + switchToPreventRxState + 'return "SYMBOL";' | ||
], | ||
[['suggestPointWhenWhitespace'], | ||
'{ws}', | ||
'this.popState(); this.begin("suggestPoint");' | ||
], | ||
[['suggestPointVar'], | ||
'(?=({ows}{comment})*{ows}([:;]|$))', | ||
// FIXME: using `this.done = false;` is hack, since `regexp-lexer` set done=true | ||
// when no input left and doesn't take into account current state; | ||
// should be fixed in `regexp-lexer` | ||
'this.popState(); this.done = false; yytext = "_"; ' + switchToPreventRxState + 'return "SYMBOL";' | ||
], | ||
[['suggestPointVarDisabled'], | ||
'(?=;)', | ||
'this.popState();' | ||
], | ||
[['suggestPoint', 'suggestPointWhenWhitespace', 'suggestPointVar'], | ||
'', | ||
'this.popState();' | ||
] | ||
].concat( | ||
grammar.lex.rules.map(entry => { | ||
let [sc, rule, action] = entry.length === 3 ? entry : [undefined, ...entry]; | ||
if (rule === '\\$') { | ||
action = `if (this.conditionStack.indexOf("suggestPointVarDisabled") === -1) this.begin("suggestPointVar"); ${action}`; | ||
} else if (tolerantScopeStart.has(rule)) { | ||
action = `this.begin("suggestPoint${ | ||
rule.endsWith('{wb}') ? 'WhenWhitespace' : '' | ||
}"); ${action}`; | ||
} | ||
if (rule === ':') { | ||
action = 'this.begin("suggestPointVarDisabled"); ' + action; | ||
} | ||
return entry.length === 3 ? [sc, rule, action] : [rule, action]; | ||
}) | ||
) | ||
}) | ||
}); | ||
// guard to keep in sync tolerantScopeStart and lex rules | ||
tolerantScopeStart.forEach(rule => { | ||
if (!tolerantGrammar.lex.rules.some(lexRule => lexRule[0] === rule)) { | ||
throw new Error('Rule missed in lexer: "' + rule.replace(/\\/g, '\\\\') + '"'); | ||
} | ||
}); | ||
const strictParser = new Parser(grammar); | ||
const tolerantParser = new Parser(tolerantGrammar); | ||
// tolerantParser.lexer.setInput('$;'); | ||
// while (!tolerantParser.lexer.done) { | ||
// console.log(tolerantParser.lexer.conditionStack); | ||
// console.log('>>', tolerantParser.lexer.next()); | ||
// } | ||
// process.exit(); | ||
module.exports = strictParser; | ||
module.exports.strict = strictParser; | ||
module.exports.tolerant = tolerantParser; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Uses eval
Supply chain riskPackage uses dynamic code execution (e.g., eval()), which is a dangerous practice. This can prevent the code from running in certain environments and increases the risk that the code may contain exploits or malicious behavior.
Found 1 instance in 1 package
217480
11
3234
246
7