Comparing version 0.0.1 to 0.1.0
{ | ||
"name": "groq-js", | ||
"version": "0.0.1", | ||
"version": "0.1.0", | ||
"main": "src/index.js", | ||
"scripts": { | ||
"test": "jest" | ||
"test": "jest", | ||
"build-api": "jsdoc2md -t utils/docs.hbs 'src/**/*.js' > API.md", | ||
"prettify": "prettier --write src/**/*.js" | ||
}, | ||
"prettier": { | ||
"semi": false, | ||
"printWidth": 100, | ||
"bracketSpacing": false, | ||
"singleQuote": true | ||
}, | ||
"devDependencies": { | ||
"jest": "^24.8.0", | ||
"jsdoc-to-markdown": "^5.0.1", | ||
"ndjson": "^1.5.0", | ||
"prettier": "^1.18.2" | ||
}, | ||
"files": ["src", "README.md"] | ||
"files": [ | ||
"src", | ||
"README.md" | ||
] | ||
} |
@@ -19,3 +19,8 @@ # GROQ-JS | ||
// Evaluate a tree against a set of documents | ||
let result = await evaluate(tree, {documents}) | ||
let value = await evaluate(tree, {documents}) | ||
// Gather everything into one JavaScript object | ||
let result = await value.get() | ||
console.log(result) | ||
``` | ||
@@ -26,4 +31,6 @@ | ||
- [Installation](#installation) | ||
- [Documentation](#documentation) | ||
- [Versioning](#versioning) | ||
- [License](#license) | ||
- [Tests](#tests) | ||
@@ -36,14 +43,52 @@ ## Installation | ||
# NPM | ||
npm i git+https://git@github.com/sanity-io/groq-js.git | ||
npm i groq-js | ||
# Yarn | ||
yarn add git+https://git@github.com/sanity-io/groq-js.git | ||
yarn add groq-js | ||
``` | ||
## Documentation | ||
See [API.md](API.md) for the public API. | ||
## Versioning | ||
GROQ-JS is currently not released. | ||
GROQ-JS follows [SemVer](https://semver.org) and is currently at version v0.1. | ||
This is an "experimental" release and anything *may* change at any time, but we're trying to keep changes as minimal as possible: | ||
- The public API of the parser/evaluator will most likely stay the same in future version. | ||
- The syntax tree is *not* considered a public API and may change at any time. | ||
- This package always implements the latest version of [GROQ according to the specification](https://github.com/sanity-io/groq). | ||
- The goal is to release a v1.0.0 by the end of 2019. | ||
## License | ||
MIT © [Sanity.io](https://www.sanity.io/) | ||
MIT © [Sanity.io](https://www.sanity.io/) | ||
## Tests | ||
Tests are written in [Jest](https://jestjs.io/): | ||
```bash | ||
# Install dependencies | ||
yarn | ||
# Run tests | ||
yarn test | ||
``` | ||
You can also generate tests from [the official GROQ test suite](https://github.com/sanity-io/groq-test-suite): | ||
```bash | ||
# Clone the repo somewhere: | ||
git clone https://github.com/sanity-io/groq-test-suite somewhere | ||
# Install dependencies: | ||
(cd somewhere && yarn) | ||
# Generate test file (in this repo): | ||
./test/generate.sh somewhere | ||
# Run tests as usual: | ||
yarn test | ||
``` |
@@ -1,23 +0,198 @@ | ||
const Value = require('./value') | ||
const {StaticValue, getType, fromNumber, TRUE_VALUE, FALSE_VALUE, NULL_VALUE} = require('./value') | ||
const {totalCompare} = require('./ordering') | ||
exports.count = function count(args, scope, execute) { | ||
if (args.length !== 1) throw new Error("count: 1 argument required") | ||
const functions = (exports.functions = {}) | ||
const pipeFunctions = (exports.pipeFunctions = {}) | ||
return new Value(async () => { | ||
functions.coalesce = async function coalesce(args, scope, execute) { | ||
for (let arg of args) { | ||
let value = await execute(arg, scope) | ||
if (value.getType() != 'null') return value | ||
} | ||
return NULL_VALUE | ||
} | ||
functions.count = async function count(args, scope, execute) { | ||
if (args.length !== 1) return NULL_VALUE | ||
let inner = await execute(args[0], scope) | ||
if (inner.getType() != 'array') return NULL_VALUE | ||
let num = 0 | ||
for await (let _ of inner) { | ||
num++ | ||
} | ||
return new StaticValue(num) | ||
} | ||
functions.defined = async function defined(args, scope, execute) { | ||
if (args.length !== 1) return NULL_VALUE | ||
let inner = await execute(args[0], scope) | ||
return inner.getType() == 'null' ? FALSE_VALUE : TRUE_VALUE | ||
} | ||
functions.identity = async function identity(args, scope, execute) { | ||
if (args.length !== 0) return NULL_VALUE | ||
return new StaticValue('me') | ||
} | ||
function countUTF8(str) { | ||
let count = 0 | ||
for (let i = 0; i < str.length; i++) { | ||
let code = str.charCodeAt(i) | ||
if (code >= 0xd800 && code <= 0xdbff) { | ||
// High surrogate. Don't count this. | ||
// By only counting the low surrogate we will correctly | ||
// count the number of UTF-8 code points. | ||
continue | ||
} | ||
count++ | ||
} | ||
return count | ||
} | ||
functions.length = async function length(args, scope, execute) { | ||
if (args.length !== 1) return NULL_VALUE | ||
let inner = await execute(args[0], scope) | ||
if (inner.getType() == 'string') { | ||
let data = await inner.get() | ||
return fromNumber(countUTF8(data)) | ||
} | ||
if (inner.getType() == 'array') { | ||
let num = 0 | ||
let inner = execute(args[0], scope) | ||
for await (let _ of inner) { | ||
num++ | ||
} | ||
return num | ||
}) | ||
return fromNumber(num) | ||
} | ||
return NULL_VALUE | ||
} | ||
exports.defined = function defined(args, scope, execute) { | ||
if (args.length !== 1) throw new Error("defined: 1 argument required") | ||
functions.select = async function select(args, scope, execute) { | ||
// First check if everything is valid: | ||
let seenFallback = false | ||
for (let arg of args) { | ||
if (seenFallback) return NULL_VALUE | ||
return new Value(async () => { | ||
let inner = await execute(args[0], scope).get() | ||
return inner != null | ||
if (arg.type == 'Pair') { | ||
// This is fine. | ||
} else { | ||
seenFallback = true | ||
} | ||
} | ||
for (let arg of args) { | ||
if (arg.type == 'Pair') { | ||
let cond = await execute(arg.left, scope) | ||
if (cond.getBoolean()) { | ||
return await execute(arg.right, scope) | ||
} | ||
} else { | ||
return await execute(arg, scope) | ||
} | ||
} | ||
return NULL_VALUE | ||
} | ||
function hasReference(value, id) { | ||
switch (getType(value)) { | ||
case 'array': | ||
for (let v of value) { | ||
if (hasReference(v, id)) return true | ||
} | ||
break | ||
case 'object': | ||
if (value._ref === id) return true | ||
for (let v of Object.values(value)) { | ||
if (hasReference(v, id)) return true | ||
} | ||
break | ||
} | ||
return false | ||
} | ||
functions.references = async function references(args, scope, execute) { | ||
if (args.length != 1) return NULL_VALUE | ||
let idValue = await execute(args[0], scope) | ||
if (idValue.getType() != 'string') return NULL_VALUE | ||
let id = await idValue.get() | ||
let scopeValue = scope.value | ||
return hasReference(scopeValue, id) ? TRUE_VALUE : FALSE_VALUE | ||
} | ||
functions.round = async function round(args, scope, execute) { | ||
if (args.length < 1 || args.length > 2) return NULL_VALUE | ||
let value = await execute(args[0], scope) | ||
if (value.getType() != 'number') return NULL_VALUE | ||
let num = await value.get() | ||
let prec = 0 | ||
if (args.length == 2) { | ||
let precValue = await execute(args[1], scope) | ||
if (precValue.getType() != 'number') return NULL_VALUE | ||
prec = await precValue.get() | ||
} | ||
if (prec == 0) { | ||
return fromNumber(Math.round(num)) | ||
} else { | ||
return fromNumber(Number(num.toFixed(prec))) | ||
} | ||
} | ||
pipeFunctions.order = async function order(base, args, scope, execute) { | ||
if (args.length == 0) throw new Error('order: at least one argument required') | ||
if (base.getType() != 'array') return NULL_VALUE | ||
let mappers = [] | ||
let directions = [] | ||
let n = 0 | ||
for (let mapper of args) { | ||
let direction = 'asc' | ||
if (mapper.type == 'Desc') { | ||
direction = 'desc' | ||
mapper = mapper.base | ||
} else if (mapper.type == 'Asc') { | ||
mapper = mapper.base | ||
} | ||
mappers.push(mapper) | ||
directions.push(direction) | ||
n++ | ||
} | ||
let aux = [] | ||
for await (let value of base) { | ||
let newScope = scope.createNested(value) | ||
let tuple = [await value.get()] | ||
for (let i = 0; i < n; i++) { | ||
let result = await execute(mappers[i], newScope) | ||
tuple.push(await result.get()) | ||
} | ||
aux.push(tuple) | ||
} | ||
aux.sort((aTuple, bTuple) => { | ||
for (let i = 0; i < n; i++) { | ||
let c = totalCompare(aTuple[i + 1], bTuple[i + 1]) | ||
if (directions[i] == 'desc') c = -c | ||
if (c != 0) return c | ||
} | ||
return 0 | ||
}) | ||
} | ||
return new StaticValue(aux.map(v => v[0])) | ||
} |
@@ -1,5 +0,29 @@ | ||
const Value = require('./value') | ||
const functions = require('./functions') | ||
const { | ||
StaticValue, | ||
StreamValue, | ||
MapperValue, | ||
NULL_VALUE, | ||
TRUE_VALUE, | ||
FALSE_VALUE, | ||
Range, | ||
Pair, | ||
fromNumber | ||
} = require('./value') | ||
const {functions, pipeFunctions} = require('./functions') | ||
const operators = require('./operators') | ||
function inMapper(value, fn) { | ||
if (value instanceof MapperValue) { | ||
return new MapperValue( | ||
new StreamValue(async function*() { | ||
for await (let elementValue of value) { | ||
yield await fn(elementValue) | ||
} | ||
}) | ||
) | ||
} else { | ||
return fn(value) | ||
} | ||
} | ||
class Scope { | ||
@@ -26,11 +50,15 @@ constructor(params, source, value, parent) { | ||
This(_, scope) { | ||
return new Value(scope.value) | ||
return scope.value | ||
}, | ||
Star(_, scope) { | ||
return scope.source.createSink() | ||
return scope.source | ||
}, | ||
Parent(_, scope) { | ||
return new Value(scope.parent && scope.parent.value) | ||
Parent({n}, scope) { | ||
for (let i = 0; i < n; i++) { | ||
scope = scope.parent | ||
if (!scope) return NULL_VALUE | ||
} | ||
return scope.value | ||
}, | ||
@@ -40,3 +68,3 @@ | ||
let func = operators[op] | ||
if (!func) throw new Error("Unknown operator: " + op) | ||
if (!func) throw new Error('Unknown operator: ' + op) | ||
return func(left, right, scope, execute) | ||
@@ -47,29 +75,51 @@ }, | ||
let func = functions[name] | ||
if (!func) throw new Error("Unknown function: " + name) | ||
if (!func) throw new Error('Unknown function: ' + name) | ||
return func(args, scope, execute) | ||
}, | ||
Filter({base, query}, scope) { | ||
return new Value(async function*() { | ||
let b = execute(base, scope) | ||
for await (let value of b) { | ||
let newScope = scope.createNested(value) | ||
let didMatch = await execute(query, newScope).get() | ||
if (didMatch) yield value | ||
} | ||
}) | ||
async PipeFuncCall({base, name, args}, scope) { | ||
let func = pipeFunctions[name] | ||
if (!func) throw new Error('Unknown function: ' + name) | ||
let baseValue = await execute(base, scope) | ||
return func(baseValue, args, scope, execute) | ||
}, | ||
Identifier({name}, scope) { | ||
return new Value(name in scope.value ? scope.value[name] : null) | ||
async Filter({base, query}, scope) { | ||
let baseValue = await execute(base, scope) | ||
return inMapper(baseValue, async value => { | ||
if (value.getType() != 'array') return NULL_VALUE | ||
return new StreamValue(async function*() { | ||
for await (let element of value) { | ||
let newScope = scope.createNested(element) | ||
let condValue = await execute(query, newScope) | ||
if (condValue.getBoolean()) yield element | ||
} | ||
}) | ||
}) | ||
}, | ||
GetIdentifier({base, name}, scope) { | ||
return new Value(async () => { | ||
let obj = await execute(base, scope).get() | ||
async Element({base, index}, scope) { | ||
let baseValue = await execute(base, scope) | ||
if (obj && typeof obj === 'object') { | ||
return obj[name] | ||
return inMapper(baseValue, async arrayValue => { | ||
if (arrayValue.getType() != 'array') return NULL_VALUE | ||
let idxValue = await execute(index, scope) | ||
if (idxValue.getType() != 'number') return NULL_VALUE | ||
// OPT: Here we can optimize when idx >= 0 | ||
let array = await arrayValue.get() | ||
let idx = await idxValue.get() | ||
if (idx < 0) { | ||
idx = array.length + idx | ||
} | ||
if (idx >= 0 && idx < array.length) { | ||
return new StaticValue(array[idx]) | ||
} else { | ||
return null | ||
// Make sure we return `null` for out-of-bounds access | ||
return NULL_VALUE | ||
} | ||
@@ -79,68 +129,192 @@ }) | ||
Value({value}) { | ||
return new Value(value) | ||
async Slice({base, left, right, isExclusive}, scope) { | ||
let baseValue = await execute(base, scope) | ||
return inMapper(baseValue, async arrayValue => { | ||
if (arrayValue.getType() != 'array') return NULL_VALUE | ||
let leftIdxValue = await execute(left, scope) | ||
let rightIdxValue = await execute(right, scope) | ||
if (leftIdxValue.getType() != 'number' || rightIdxValue.getType() != 'number') { | ||
return NULL_VALUE | ||
} | ||
// OPT: Here we can optimize when either indices are >= 0 | ||
let array = await arrayValue.get() | ||
let leftIdx = await leftIdxValue.get() | ||
let rightIdx = await rightIdxValue.get() | ||
// Handle negative index | ||
if (leftIdx < 0) leftIdx = array.length + leftIdx | ||
if (rightIdx < 0) rightIdx = array.length + rightIdx | ||
// Convert from inclusive to exclusive index | ||
if (!isExclusive) rightIdx++ | ||
if (leftIdx < 0) leftIdx = 0 | ||
if (rightIdx < 0) rightIdx = 0 | ||
// Note: At this point the indices might point out-of-bound, but | ||
// .slice handles this correctly. | ||
return new StaticValue(array.slice(leftIdx, rightIdx)) | ||
}) | ||
}, | ||
Project({base, query}, scope) { | ||
let b = execute(base, scope) | ||
return new Value(async function*() { | ||
for await (let data of b) { | ||
let newScope = scope.createNested(data) | ||
let newData = await execute(query, newScope).get() | ||
yield newData | ||
async Attribute({base, name}, scope) { | ||
let baseValue = await execute(base, scope) | ||
return inMapper(baseValue, async value => { | ||
if (value.getType() == 'object') { | ||
let data = await value.get() | ||
if (data.hasOwnProperty(name)) { | ||
return new StaticValue(data[name]) | ||
} | ||
} | ||
return NULL_VALUE | ||
}) | ||
}, | ||
ArrProject({base, query}, scope) { | ||
let b = execute(base, scope) | ||
return new Value(async function*() { | ||
for await (let data of b) { | ||
let newScope = scope.createNested(data) | ||
let newData = await execute(query, newScope).get() | ||
yield newData | ||
async Identifier({name}, scope) { | ||
if (scope.value.getType() == 'object') { | ||
let data = await scope.value.get() | ||
if (data.hasOwnProperty(name)) { | ||
return new StaticValue(data[name]) | ||
} | ||
} | ||
return NULL_VALUE | ||
}, | ||
Value({value}) { | ||
return new StaticValue(value) | ||
}, | ||
async Mapper({base}, scope) { | ||
let baseValue = await execute(base, scope) | ||
if (baseValue.getType() != 'array') return baseValue | ||
if (baseValue instanceof MapperValue) { | ||
return new MapperValue( | ||
new StreamValue(async function*() { | ||
for await (let element of baseValue) { | ||
if (element.getType() == 'array') { | ||
for await (let subelement of element) { | ||
yield subelement | ||
} | ||
} else { | ||
yield NULL_VALUE | ||
} | ||
} | ||
}) | ||
) | ||
} else { | ||
return new MapperValue(baseValue) | ||
} | ||
}, | ||
async Parenthesis({base}, scope) { | ||
let baseValue = await execute(base, scope) | ||
if (baseValue instanceof MapperValue) { | ||
baseValue = baseValue.value | ||
} | ||
return baseValue | ||
}, | ||
async Projection({base, query}, scope) { | ||
let baseValue = await execute(base, scope) | ||
return inMapper(baseValue, async baseValue => { | ||
if (baseValue.getType() == 'null') return NULL_VALUE | ||
if (baseValue.getType() == 'array') { | ||
return new StreamValue(async function*() { | ||
for await (let value of baseValue) { | ||
let newScope = scope.createNested(value) | ||
let newValue = await execute(query, newScope) | ||
yield newValue | ||
} | ||
}) | ||
} else { | ||
let newScope = scope.createNested(baseValue) | ||
return await execute(query, newScope) | ||
} | ||
}) | ||
}, | ||
Deref({base}, scope) { | ||
return new Value(async function() { | ||
let ref = await execute(base, scope).get() | ||
if (!ref) return | ||
async Deref({base}, scope) { | ||
let baseValue = await execute(base, scope) | ||
return inMapper(baseValue, async baseValue => { | ||
if (baseValue.getType() != 'object') return NULL_VALUE | ||
for await (let doc of scope.source.createSink()) { | ||
if (typeof doc._id === 'string' && ref._ref === doc._id) { | ||
let id = (await baseValue.get())._ref | ||
if (typeof id != 'string') return NULL_VALUE | ||
for await (let doc of scope.source) { | ||
if (id === doc.data._id) { | ||
return doc | ||
} | ||
} | ||
return NULL_VALUE | ||
}) | ||
}, | ||
Object({properties}, scope) { | ||
return new Value(async () => { | ||
let result = {} | ||
for (let prop of properties) { | ||
switch (prop.type) { | ||
case 'ObjectSplat': | ||
Object.assign(result, scope.value) | ||
break | ||
async Object({attributes}, scope) { | ||
let result = {} | ||
for (let attr of attributes) { | ||
switch (attr.type) { | ||
case 'ObjectAttribute': { | ||
let key = await execute(attr.key, scope) | ||
if (key.getType() != 'string') continue | ||
case 'Property': | ||
let key = await execute(prop.key, scope).get() | ||
let value = await execute(prop.value, scope).get() | ||
result[key] = value | ||
let value = await execute(attr.value, scope) | ||
if (value.getType() == 'null') { | ||
delete result[key.data] | ||
break | ||
} | ||
default: | ||
throw new Error("Unknown node type: " + prop.type) | ||
result[key.data] = await value.get() | ||
break | ||
} | ||
case 'ObjectConditionalSplat': { | ||
let cond = await execute(attr.condition, scope) | ||
if (!cond.getBoolean()) continue | ||
let value = await execute(attr.value, scope) | ||
if (value.getType() != 'object') continue | ||
Object.assign(result, value.data) | ||
break | ||
} | ||
case 'ObjectSplat': { | ||
let value = await execute(attr.value, scope) | ||
if (value.getType('object')) { | ||
Object.assign(result, value.data) | ||
} | ||
break | ||
} | ||
default: | ||
throw new Error('Unknown node type: ' + attr.type) | ||
} | ||
return result | ||
}) | ||
} | ||
return new StaticValue(result) | ||
}, | ||
Array({elements}, scope) { | ||
return new Value(async function*() { | ||
return new StreamValue(async function*() { | ||
for (let element of elements) { | ||
yield await execute(element, scope).get() | ||
let value = await execute(element.value, scope) | ||
if (element.isSplat) { | ||
if (value.getType() == 'array') { | ||
for await (let v of value) { | ||
yield v | ||
} | ||
} | ||
} else { | ||
yield value | ||
} | ||
} | ||
@@ -150,37 +324,125 @@ }) | ||
And({left, right}, scope) { | ||
return new Value(async () => { | ||
let leftData = await execute(left, scope).get() | ||
if (leftData === false) return false | ||
let rightData = await execute(right, scope).get() | ||
// TODO: Correct boolean semantics | ||
return rightData | ||
}) | ||
} | ||
} | ||
async Range({left, right, isExclusive}, scope) { | ||
let leftValue = await execute(left, scope) | ||
let rightValue = await execute(right, scope) | ||
class StaticSource { | ||
constructor(documents) { | ||
this.documents = documents | ||
} | ||
if (!Range.isConstructible(leftValue.getType(), rightValue.getType())) { | ||
return NULL_VALUE | ||
} | ||
createSink() { | ||
return new Value(this.documents) | ||
let range = new Range(await leftValue.get(), await rightValue.get(), isExclusive) | ||
return new StaticValue(range) | ||
}, | ||
async Pair({left, right}, scope) { | ||
let leftValue = await execute(left, scope) | ||
let rightValue = await execute(right, scope) | ||
let pair = new Pair(await leftValue.get(), await rightValue.get()) | ||
return new StaticValue(pair) | ||
}, | ||
async Or({left, right}, scope) { | ||
let leftValue = await execute(left, scope) | ||
let rightValue = await execute(right, scope) | ||
if (leftValue.getType() == 'boolean') { | ||
if (leftValue.data == true) return TRUE_VALUE | ||
} | ||
if (rightValue.getType() == 'boolean') { | ||
if (rightValue.data == true) return TRUE_VALUE | ||
} | ||
if (leftValue.getType() != 'boolean') return NULL_VALUE | ||
if (rightValue.getType() != 'boolean') return NULL_VALUE | ||
return FALSE_VALUE | ||
}, | ||
async And({left, right}, scope) { | ||
let leftValue = await execute(left, scope) | ||
let rightValue = await execute(right, scope) | ||
if (leftValue.getType() == 'boolean') { | ||
if (leftValue.data == false) return FALSE_VALUE | ||
} | ||
if (rightValue.getType() == 'boolean') { | ||
if (rightValue.data == false) return FALSE_VALUE | ||
} | ||
if (leftValue.getType() != 'boolean') return NULL_VALUE | ||
if (rightValue.getType() != 'boolean') return NULL_VALUE | ||
return TRUE_VALUE | ||
}, | ||
async Not({base}, scope) { | ||
let value = await execute(base, scope) | ||
if (value.getType() != 'boolean') { | ||
return NULL_VALUE | ||
} | ||
return value.getBoolean() ? FALSE_VALUE : TRUE_VALUE | ||
}, | ||
async Neg({base}, scope) { | ||
let value = await execute(base, scope) | ||
if (value.getType() != 'number') return NULL_VALUE | ||
return fromNumber(-(await value.get())) | ||
}, | ||
async Pos({base}, scope) { | ||
let value = await execute(base, scope) | ||
if (value.getType() != 'number') return NULL_VALUE | ||
return fromNumber(await value.get()) | ||
}, | ||
async Asc() { | ||
return NULL_VALUE | ||
}, | ||
async Desc() { | ||
return NULL_VALUE | ||
} | ||
} | ||
function evaluate(tree, options = {}) { | ||
function isIterator(obj) { | ||
return obj != null && typeof obj.next == 'function' | ||
} | ||
/** | ||
* Evaluates a syntax tree (which you can get from {@link module:groq-js.parse}). | ||
* | ||
* @param {SyntaxNode} tree | ||
* @param {object} [options] Options. | ||
* @param {object} [options.params] Parameters availble in the GROQ query (using `$param` syntax). | ||
* @param {array | async-iterator} [options.documents] The documents that will be available as `*` in GROQ. | ||
* @return {Value} | ||
* @alias module:groq-js.evaluate | ||
*/ | ||
async function evaluate(tree, options = {}) { | ||
let source | ||
let params = {identity: 'groot'} | ||
let root = NULL_VALUE | ||
let params = {} | ||
if (options.documents != null) { | ||
if (!Array.isArray(options.documents)) { | ||
throw new Error('documents must be an array') | ||
} | ||
source = new StaticSource(options.documents) | ||
if (options.documents == null) { | ||
source = new StaticValue([]) | ||
} else if (Array.isArray(options.documents)) { | ||
source = new StaticValue(options.documents) | ||
} else if (isIterator(options.documents)) { | ||
let iter = options.documents | ||
source = new StreamValue(async function*() { | ||
for await (let value of iter) { | ||
yield new StaticValue(value) | ||
} | ||
}) | ||
} else { | ||
source = new StaticSource([]) | ||
throw new Error('documents must be an array or an iterable') | ||
} | ||
if (options.root != null) { | ||
root = new StaticValue(options.root) | ||
} | ||
if (options.params) { | ||
@@ -190,6 +452,6 @@ Object.assign(params, options.params) | ||
let scope = new Scope(params, source, null, null) | ||
return execute(tree, scope) | ||
let scope = new Scope(params, source, root, null) | ||
return await execute(tree, scope) | ||
} | ||
exports.evaluate = evaluate |
@@ -1,69 +0,181 @@ | ||
const Value = require('./value') | ||
const {StaticValue, TRUE_VALUE, FALSE_VALUE, NULL_VALUE, fromNumber} = require('./value') | ||
const isEqual = require('./equality') | ||
const {partialCompare} = require('./ordering') | ||
exports['=='] = function count(left, right, scope, execute) { | ||
return new Value(async () => { | ||
let a = await execute(left, scope).get() | ||
let b = await execute(right, scope).get() | ||
function isComparable(a, b) { | ||
let aType = a.getType() | ||
let bType = b.getType() | ||
return aType == bType && (aType == 'number' || aType == 'string' || aType == 'boolean') | ||
} | ||
return a == b | ||
}) | ||
exports['=='] = async function eq(left, right, scope, execute) { | ||
let a = await execute(left, scope) | ||
let b = await execute(right, scope) | ||
let result = await isEqual(a, b) | ||
return result ? TRUE_VALUE : FALSE_VALUE | ||
} | ||
exports['!='] = function count(left, right, scope, execute) { | ||
return new Value(async () => { | ||
let a = await execute(left, scope).get() | ||
let b = await execute(right, scope).get() | ||
exports['!='] = async function neq(left, right, scope, execute) { | ||
let a = await execute(left, scope) | ||
let b = await execute(right, scope) | ||
let result = await isEqual(a, b) | ||
return result ? FALSE_VALUE : TRUE_VALUE | ||
} | ||
return a != b | ||
}) | ||
exports['>'] = async function gt(left, right, scope, execute) { | ||
let a = await (await execute(left, scope)).get() | ||
let b = await (await execute(right, scope)).get() | ||
let result = partialCompare(a, b) | ||
if (result == null) { | ||
return NULL_VALUE | ||
} else { | ||
return result > 0 ? TRUE_VALUE : FALSE_VALUE | ||
} | ||
} | ||
exports['>'] = function count(left, right, scope, execute) { | ||
return new Value(async () => { | ||
let a = await execute(left, scope).get() | ||
let b = await execute(right, scope).get() | ||
exports['>='] = async function gte(left, right, scope, execute) { | ||
let a = await (await execute(left, scope)).get() | ||
let b = await (await execute(right, scope)).get() | ||
let result = partialCompare(a, b) | ||
return a > b | ||
}) | ||
if (result == null) { | ||
return NULL_VALUE | ||
} else { | ||
return result >= 0 ? TRUE_VALUE : FALSE_VALUE | ||
} | ||
} | ||
exports['>='] = function count(left, right, scope, execute) { | ||
return new Value(async () => { | ||
let a = await execute(left, scope).get() | ||
let b = await execute(right, scope).get() | ||
exports['<'] = async function lt(left, right, scope, execute) { | ||
let a = await (await execute(left, scope)).get() | ||
let b = await (await execute(right, scope)).get() | ||
let result = partialCompare(a, b) | ||
return a >= b | ||
}) | ||
if (result == null) { | ||
return NULL_VALUE | ||
} else { | ||
return result < 0 ? TRUE_VALUE : FALSE_VALUE | ||
} | ||
} | ||
exports['<'] = function count(left, right, scope, execute) { | ||
return new Value(async () => { | ||
let a = await execute(left, scope).get() | ||
let b = await execute(right, scope).get() | ||
exports['<='] = async function lte(left, right, scope, execute) { | ||
let a = await (await execute(left, scope)).get() | ||
let b = await (await execute(right, scope)).get() | ||
let result = partialCompare(a, b) | ||
return a < b | ||
}) | ||
if (result == null) { | ||
return NULL_VALUE | ||
} else { | ||
return result <= 0 ? TRUE_VALUE : FALSE_VALUE | ||
} | ||
} | ||
exports['<='] = function count(left, right, scope, execute) { | ||
return new Value(async () => { | ||
let a = await execute(left, scope).get() | ||
let b = await execute(right, scope).get() | ||
exports['in'] = async function inop(left, right, scope, execute) { | ||
let a = await execute(left, scope) | ||
let choices = await execute(right, scope) | ||
return a <= b | ||
switch (choices.getType()) { | ||
case 'array': | ||
for await (let b of choices) { | ||
if (await isEqual(a, b)) { | ||
return TRUE_VALUE | ||
} | ||
} | ||
return FALSE_VALUE | ||
case 'range': | ||
let value = await a.get() | ||
let range = await choices.get() | ||
let leftCmp = partialCompare(value, range.left) | ||
if (leftCmp == null) return NULL_VALUE | ||
let rightCmp = partialCompare(value, range.right) | ||
if (rightCmp == null) return NULL_VALUE | ||
if (range.isExclusive()) { | ||
return leftCmp >= 0 && rightCmp < 0 ? TRUE_VALUE : FALSE_VALUE | ||
} else { | ||
return leftCmp >= 0 && rightCmp <= 0 ? TRUE_VALUE : FALSE_VALUE | ||
} | ||
} | ||
return NULL_VALUE | ||
} | ||
async function gatherText(value, cb) { | ||
switch (value.getType()) { | ||
case 'string': | ||
cb(await value.get()) | ||
return true | ||
case 'array': | ||
for await (let part of value) { | ||
if (part.getType() == 'string') { | ||
cb(await part.get()) | ||
} else { | ||
return false | ||
} | ||
} | ||
return true | ||
} | ||
return false | ||
} | ||
exports['match'] = async function match(left, right, scope, execute) { | ||
let text = await execute(left, scope) | ||
let pattern = await execute(right, scope) | ||
let tokens = [] | ||
let patterns = [] | ||
let didSucceed = await gatherText(text, part => { | ||
tokens = tokens.concat(part.match(/[A-Za-z0-9]+/g)) | ||
}) | ||
if (!didSucceed) return NULL_VALUE | ||
didSucceed = await gatherText(pattern, part => { | ||
patterns = patterns.concat(part.match(/[A-Za-z0-9*]+/g)) | ||
}) | ||
if (!didSucceed) return NULL_VALUE | ||
let matched = patterns.every(p => { | ||
let regexp = new RegExp('^' + p.replace('*', '.*') + '$', 'i') | ||
return tokens.some(token => regexp.test(token)) | ||
}) | ||
return matched ? TRUE_VALUE : FALSE_VALUE | ||
} | ||
exports['in'] = function count(left, right, scope, execute) { | ||
return new Value(async () => { | ||
let a = await execute(left, scope).get() | ||
for await (let b of execute(right, scope)) { | ||
if (a == b) { | ||
return true | ||
} | ||
exports['+'] = async function plus(left, right, scope, execute) { | ||
let a = await execute(left, scope) | ||
let b = await execute(right, scope) | ||
let aType = a.getType() | ||
let bType = b.getType() | ||
if ((aType == 'number' && bType == 'number') || (aType == 'string' && bType == 'string')) { | ||
return new StaticValue((await a.get()) + (await b.get())) | ||
} | ||
return NULL_VALUE | ||
} | ||
function numericOperator(impl) { | ||
return async function(left, right, scope, execute) { | ||
let a = await execute(left, scope) | ||
let b = await execute(right, scope) | ||
let aType = a.getType() | ||
let bType = b.getType() | ||
if (aType == 'number' && bType == 'number') { | ||
let result = impl(await a.get(), await b.get()) | ||
return fromNumber(result) | ||
} | ||
return false | ||
}) | ||
} | ||
return NULL_VALUE | ||
} | ||
} | ||
exports['-'] = numericOperator((a, b) => a - b) | ||
exports['*'] = numericOperator((a, b) => a * b) | ||
exports['/'] = numericOperator((a, b) => a / b) | ||
exports['%'] = numericOperator((a, b) => a % b) | ||
exports['**'] = numericOperator((a, b) => Math.pow(a, b)) |
@@ -1,75 +0,219 @@ | ||
const ArrayIterator = Array.prototype[Symbol.iterator] | ||
const getType = (exports.getType = function getType(data) { | ||
if (data == null) return 'null' | ||
if (Array.isArray(data)) return 'array' | ||
if (data instanceof Range) return 'range' | ||
return typeof data | ||
}) | ||
function isIterator(obj) { | ||
return obj && typeof obj.next === 'function' | ||
} | ||
/** | ||
* A type of a value in GROQ. | ||
* | ||
* This can be one of: | ||
* - 'null' | ||
* - 'boolean' | ||
* - 'number' | ||
* - 'string' | ||
* - 'array' | ||
* - 'object' | ||
* - 'range' | ||
* - 'pair' | ||
* @typedef {string} ValueType | ||
*/ | ||
function isPromise(obj) { | ||
return obj && typeof obj.then === 'function' | ||
} | ||
/** The result of an expression. | ||
* | ||
* @interface Value | ||
*/ | ||
void 0 | ||
const EmptyIterator = { | ||
next() { | ||
return {done: true} | ||
/** | ||
* Returns the type of the value. | ||
* @function | ||
* @name Value#getType | ||
* @return {ValueType} | ||
*/ | ||
/** | ||
* Returns a JavaScript representation of the value. | ||
* @async | ||
* @function | ||
* @return {Promise} | ||
* @name Value#get | ||
*/ | ||
class StaticValue { | ||
constructor(data) { | ||
this.data = data | ||
} | ||
getType() { | ||
return getType(this.data) | ||
} | ||
async get() { | ||
return this.data | ||
} | ||
[Symbol.asyncIterator]() { | ||
if (Array.isArray(this.data)) { | ||
return (function*(data) { | ||
for (let element of data) { | ||
yield new StaticValue(element) | ||
} | ||
})(this.data) | ||
} else { | ||
throw new Error('Cannot iterate over: ' + this.getType()) | ||
} | ||
} | ||
getBoolean() { | ||
return this.data === true | ||
} | ||
} | ||
/** A Value represents a value that can be produced during execution of a query. | ||
/** A StreamValue accepts a generator which yields values. | ||
* | ||
* Value provides a `get()` method for returning the whole data, but also | ||
* implements the async iterator protocol for streaming data. | ||
* @private | ||
*/ | ||
class Value { | ||
/** Constructs a new Value. | ||
* | ||
* The `inner` parameter can take the following types: | ||
* | ||
* (a) JSON-data | ||
* (b) Promise which resolves to JSON-data | ||
* (c) Function which returns (a) or (b). This function will be invoked synchronously. | ||
* (d) Generator function which yields JSON-data | ||
*/ | ||
constructor(inner) { | ||
this.inner = typeof inner === 'function' ? inner() : inner | ||
class StreamValue { | ||
constructor(generator) { | ||
this._generator = generator | ||
this._ticker = null | ||
this._isDone = false | ||
this._data = [] | ||
} | ||
/** Returns the data inside the Value. */ | ||
getType() { | ||
return 'array' | ||
} | ||
async get() { | ||
if (isIterator(this.inner)) { | ||
let result = [] | ||
for await (let data of this.inner) { | ||
result.push(data) | ||
let result = [] | ||
for await (let value of this) { | ||
result.push(await value.get()) | ||
} | ||
return result | ||
} | ||
async *[Symbol.asyncIterator]() { | ||
let i = 0 | ||
while (true) { | ||
for (; i < this._data.length; i++) { | ||
yield this._data[i] | ||
} | ||
return result | ||
} else { | ||
return this.inner | ||
if (this._isDone) return | ||
await this._nextTick() | ||
} | ||
} | ||
/** Iterates over every element in the Value. */ | ||
[Symbol.asyncIterator]() { | ||
if (isIterator(this.inner)) { | ||
return this.inner | ||
} else if (isPromise(this.inner)) { | ||
return { | ||
iterator: null, | ||
promise: this.inner, | ||
async next() { | ||
if (!this.iterator) { | ||
let inner = await this.promise | ||
this.iterator = ArrayIterator.call(inner) | ||
} | ||
return this.iterator.next() | ||
} | ||
getBoolean() { | ||
return false | ||
} | ||
_nextTick() { | ||
if (this._ticker) return this._ticker | ||
let currentResolver | ||
let setupTicker = () => { | ||
this._ticker = new Promise(resolve => { | ||
currentResolver = resolve | ||
}) | ||
} | ||
let tick = () => { | ||
currentResolver() | ||
setupTicker() | ||
} | ||
let fetch = async () => { | ||
for await (let value of this._generator()) { | ||
this._data.push(value) | ||
tick() | ||
} | ||
} else { | ||
if (Array.isArray(this.inner)) { | ||
return ArrayIterator.call(this.inner) | ||
} else { | ||
return EmptyIterator | ||
} | ||
this._isDone = true | ||
tick() | ||
} | ||
setupTicker() | ||
fetch() | ||
return this._ticker | ||
} | ||
} | ||
module.exports = Value | ||
class MapperValue { | ||
constructor(value) { | ||
this.value = value | ||
} | ||
getType() { | ||
return 'array' | ||
} | ||
async get() { | ||
return await this.value.get() | ||
} | ||
[Symbol.asyncIterator]() { | ||
return this.value[Symbol.asyncIterator].call(this.value) | ||
} | ||
getBoolean() { | ||
return false | ||
} | ||
} | ||
class Range { | ||
static isConstructible(leftType, rightType) { | ||
if (leftType == rightType) { | ||
if (leftType == 'number') return true | ||
if (leftType == 'string') return true | ||
} | ||
return false | ||
} | ||
constructor(left, right, exclusive) { | ||
this.left = left | ||
this.right = right | ||
this.exclusive = exclusive | ||
} | ||
isExclusive() { | ||
return this.exclusive | ||
} | ||
toJSON() { | ||
return [this.left, this.right] | ||
} | ||
} | ||
class Pair { | ||
constructor(first, second) { | ||
this.first = first | ||
this.second = second | ||
} | ||
toJSON() { | ||
return [this.first, this.second] | ||
} | ||
} | ||
function fromNumber(num) { | ||
if (Number.isFinite(num)) { | ||
return new StaticValue(num) | ||
} else { | ||
return exports.NULL_VALUE | ||
} | ||
} | ||
exports.StaticValue = StaticValue | ||
exports.Range = Range | ||
exports.Pair = Pair | ||
exports.StreamValue = StreamValue | ||
exports.MapperValue = MapperValue | ||
exports.fromNumber = fromNumber | ||
exports.NULL_VALUE = new StaticValue(null) | ||
exports.TRUE_VALUE = new StaticValue(true) | ||
exports.FALSE_VALUE = new StaticValue(false) |
@@ -0,1 +1,5 @@ | ||
/** | ||
* @module groq-js | ||
*/ | ||
const {parse} = require('./parser') | ||
@@ -2,0 +6,0 @@ const {evaluate} = require('./evaluator') |
@@ -1,2 +0,5 @@ | ||
/** Helper class for processing a mark stream (which is what the rawParser returns). */ | ||
/** Helper class for processing a mark stream (which is what the rawParser returns). | ||
* | ||
* @private | ||
*/ | ||
class MarkProcessor { | ||
@@ -3,0 +6,0 @@ constructor(visitor, string, marks) { |
const {parse: rawParse} = require('./rawParser') | ||
const MarkProcessor = require('./markProcessor') | ||
function isNumber(node) { | ||
return node.type == 'Value' && typeof node.value == 'number' | ||
} | ||
function isString(node) { | ||
return node.type == 'Value' && typeof node.value == 'string' | ||
} | ||
/** | ||
* A tree-structure representing a GROQ query. | ||
* | ||
* @typedef {object} SyntaxNode | ||
* @property {string} type The type of the node. | ||
* @abstract | ||
*/ | ||
const BUILDER = { | ||
paren(p) { | ||
let inner = p.process() | ||
return { | ||
type: 'Parenthesis', | ||
base: inner | ||
} | ||
}, | ||
filter(p, mark) { | ||
let base = p.process() | ||
let query = p.process() | ||
return unwrapArrProjection(base, base => ({ | ||
if (isNumber(query)) { | ||
return { | ||
type: 'Element', | ||
base, | ||
index: query | ||
} | ||
} | ||
if (isString(query)) { | ||
return { | ||
type: 'Attribute', | ||
base, | ||
name: query.value | ||
} | ||
} | ||
if (query.type == 'Range') { | ||
return { | ||
type: 'Slice', | ||
base, | ||
left: query.left, | ||
right: query.right, | ||
isExclusive: query.isExclusive | ||
} | ||
} | ||
return { | ||
type: 'Filter', | ||
base, | ||
query | ||
})) | ||
} | ||
}, | ||
@@ -18,7 +69,7 @@ | ||
let query = p.process() | ||
return unwrapArrProjection(base, base => ({ | ||
type: 'Project', | ||
return { | ||
type: 'Projection', | ||
base, | ||
query | ||
})) | ||
} | ||
}, | ||
@@ -35,5 +86,16 @@ | ||
parent(p, mark) { | ||
return {type: 'Parent'} | ||
return { | ||
type: 'Parent', | ||
n: 1, | ||
} | ||
}, | ||
dblparent(p, mark) { | ||
let next = p.process() | ||
return { | ||
type: 'Parent', | ||
n: next.n + 1 | ||
} | ||
}, | ||
ident(p, mark) { | ||
@@ -56,7 +118,7 @@ let name = p.processStringEnd() | ||
return unwrapArrProjection(base, base => ({ | ||
type: 'GetIdentifier', | ||
return { | ||
type: 'Attribute', | ||
base, | ||
name | ||
})) | ||
} | ||
}, | ||
@@ -67,5 +129,4 @@ | ||
return { | ||
type: 'ArrProject', | ||
base, | ||
query: {type: 'This'} | ||
type: 'Mapper', | ||
base | ||
} | ||
@@ -80,10 +141,22 @@ }, | ||
left, | ||
right | ||
right, | ||
isExclusive: false | ||
} | ||
}, | ||
exc_range(p, mark) { | ||
let left = p.process() | ||
let right = p.process() | ||
return { | ||
type: 'Range', | ||
left, | ||
right, | ||
isExclusive: true | ||
} | ||
}, | ||
neg(p, mark) { | ||
let base = p.process() | ||
if (base.type === 'Value') { | ||
if (base.type === 'Value' && typeof base.value == 'number') { | ||
return { | ||
@@ -101,2 +174,84 @@ type: 'Value', | ||
pos(p, mark) { | ||
let base = p.process() | ||
if (base.type === 'Value' && typeof base.value == 'number') { | ||
return { | ||
type: 'Value', | ||
value: +base.value | ||
} | ||
} | ||
return { | ||
type: 'Pos', | ||
base | ||
} | ||
}, | ||
add(p, mark) { | ||
let left = p.process() | ||
let right = p.process() | ||
return { | ||
type: 'OpCall', | ||
op: '+', | ||
left, | ||
right, | ||
} | ||
}, | ||
sub(p, mark) { | ||
let left = p.process() | ||
let right = p.process() | ||
return { | ||
type: 'OpCall', | ||
op: '-', | ||
left, | ||
right, | ||
} | ||
}, | ||
mul(p, mark) { | ||
let left = p.process() | ||
let right = p.process() | ||
return { | ||
type: 'OpCall', | ||
op: '*', | ||
left, | ||
right, | ||
} | ||
}, | ||
div(p, mark) { | ||
let left = p.process() | ||
let right = p.process() | ||
return { | ||
type: 'OpCall', | ||
op: '/', | ||
left, | ||
right, | ||
} | ||
}, | ||
mod(p, mark) { | ||
let left = p.process() | ||
let right = p.process() | ||
return { | ||
type: 'OpCall', | ||
op: '%', | ||
left, | ||
right, | ||
} | ||
}, | ||
pow(p, mark) { | ||
let left = p.process() | ||
let right = p.process() | ||
return { | ||
type: 'OpCall', | ||
op: '**', | ||
left, | ||
right, | ||
} | ||
}, | ||
deref(p, mark) { | ||
@@ -106,11 +261,14 @@ let base = p.process() | ||
let nextMark = p.getMark() | ||
let result = {type: 'Deref', base} | ||
if (nextMark && nextMark.name === 'deref_field') { | ||
throw new Error('TODO: Handle deref properly') | ||
let name = p.processString() | ||
result = { | ||
type: 'Attribute', | ||
base: result, | ||
name | ||
} | ||
} | ||
return unwrapArrProjection(base, base => ({ | ||
type: 'Deref', | ||
base | ||
})) | ||
return result | ||
}, | ||
@@ -154,6 +312,24 @@ | ||
sci(p, mark) { | ||
let strValue = p.processStringEnd() | ||
return { | ||
type: 'Value', | ||
value: Number(strValue) | ||
} | ||
}, | ||
pair(p, mark) { | ||
let left = p.process() | ||
let right = p.process() | ||
return { | ||
type: 'Pair', | ||
left, | ||
right | ||
} | ||
}, | ||
object(p, mark) { | ||
let properties = [] | ||
let attributes = [] | ||
while (p.getMark().name !== 'object_end') { | ||
properties.push(p.process()) | ||
attributes.push(p.process()) | ||
} | ||
@@ -163,3 +339,3 @@ p.shift() | ||
type: 'Object', | ||
properties: properties | ||
attributes | ||
} | ||
@@ -170,4 +346,13 @@ }, | ||
let value = p.process() | ||
if (value.type == 'Pair') { | ||
return { | ||
type: 'ObjectConditionalSplat', | ||
condition: value.left, | ||
value: value.right | ||
} | ||
} | ||
return { | ||
type: 'Property', | ||
type: 'ObjectAttribute', | ||
key: { | ||
@@ -185,3 +370,3 @@ type: 'Value', | ||
return { | ||
type: 'Property', | ||
type: 'ObjectAttribute', | ||
key: key, | ||
@@ -193,11 +378,31 @@ value: value | ||
object_splat(p, mark) { | ||
let value = p.process() | ||
return { | ||
type: 'ObjectSplat' | ||
type: 'ObjectSplat', | ||
value | ||
} | ||
}, | ||
object_splat_this(p, mark) { | ||
return { | ||
type: 'ObjectSplat', | ||
value: {type: 'This'} | ||
} | ||
}, | ||
array(p, mark) { | ||
let elements = [] | ||
while (p.getMark().name !== 'array_end') { | ||
elements.push(p.process()) | ||
let isSplat = false | ||
if (p.getMark().name == 'array_splat') { | ||
isSplat = true | ||
p.shift() | ||
} | ||
let value = p.process() | ||
elements.push({ | ||
type: 'ArrayElement', | ||
value, | ||
isSplat | ||
}) | ||
} | ||
@@ -225,2 +430,12 @@ p.shift() | ||
pipecall(p, mark) { | ||
let base = p.process() | ||
let func = p.process() | ||
return { | ||
...func, | ||
type: 'PipeFuncCall', | ||
base | ||
} | ||
}, | ||
and(p, mark) { | ||
@@ -234,5 +449,50 @@ let left = p.process() | ||
} | ||
}, | ||
or(p, mark) { | ||
let left = p.process() | ||
let right = p.process() | ||
return { | ||
type: 'Or', | ||
left, | ||
right | ||
} | ||
}, | ||
not(p, mark) { | ||
let base = p.process() | ||
return { | ||
type: 'Not', | ||
base | ||
} | ||
}, | ||
asc(p, mark) { | ||
let base = p.process() | ||
return { | ||
type: 'Asc', | ||
base | ||
} | ||
}, | ||
desc(p, mark) { | ||
let base = p.process() | ||
return { | ||
type: 'Desc', | ||
base | ||
} | ||
} | ||
} | ||
const NESTED_PROPERTY_TYPES = [ | ||
'Deref', | ||
'Projection', | ||
'Mapper', | ||
'Filter', | ||
'Element', | ||
'Slice', | ||
] | ||
function extractPropertyKey(node) { | ||
@@ -243,3 +503,3 @@ if (node.type === 'Identifier') { | ||
if (node.type === 'Deref' || node.type === 'ArrProject') { | ||
if (NESTED_PROPERTY_TYPES.includes(node.type)) { | ||
return extractPropertyKey(node.base) | ||
@@ -251,14 +511,10 @@ } | ||
function unwrapArrProjection(base, func) { | ||
if (base.type === 'ArrProject') { | ||
return { | ||
type: 'ArrProject', | ||
base: base.base, | ||
query: func(base.query) | ||
} | ||
} else { | ||
return func(base) | ||
} | ||
} | ||
/** | ||
* Parses a GROQ query and returns a tree structure. | ||
* | ||
* @param {string} input GROQ query | ||
* @returns {SyntaxNode} | ||
* @alias module:groq-js.parse | ||
* @static | ||
*/ | ||
function parse(input) { | ||
@@ -265,0 +521,0 @@ let result = rawParse(input) |
Sorry, the diff of this file is too big to display
Misc. License Issues
License(Experimental) A package's licensing information has fine-grained problems.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
120706
12
4136
91
4
1
1