added all files to project

This commit is contained in:
2022-03-10 10:36:59 +01:00
parent 09dd957b33
commit 46a936d7de
25351 changed files with 3883356 additions and 0 deletions
+22
View File
@@ -0,0 +1,22 @@
Copyright (c) 2019 Lukas Siemon
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
+913
View File
@@ -0,0 +1,913 @@
# Object-Scan
[![Build Status](https://circleci.com/gh/blackflux/object-scan.png?style=shield)](https://circleci.com/gh/blackflux/object-scan)
[![Test Coverage](https://img.shields.io/coveralls/blackflux/object-scan/master.svg)](https://coveralls.io/github/blackflux/object-scan?branch=master)
[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=blackflux/object-scan)](https://dependabot.com)
[![Dependencies](https://david-dm.org/blackflux/object-scan/status.svg)](https://david-dm.org/blackflux/object-scan)
[![NPM](https://img.shields.io/npm/v/object-scan.svg)](https://www.npmjs.com/package/object-scan)
[![Downloads](https://img.shields.io/npm/dt/object-scan.svg)](https://www.npmjs.com/package/object-scan)
[![Semantic-Release](https://github.com/blackflux/js-gardener/blob/master/assets/icons/semver.svg)](https://github.com/semantic-release/semantic-release)
[![Gardener](https://github.com/blackflux/js-gardener/blob/master/assets/badge.svg)](https://github.com/blackflux/js-gardener)
Traverse object hierarchies using matching and callbacks.
## Install
Install with [npm](https://www.npmjs.com/):
$ npm install --save object-scan
## Usage
<!-- eslint-disable-next-line import/no-unresolved, import/no-extraneous-dependencies -->
```js
const objectScan = require('object-scan');
const haystack = { a: { b: { c: 'd' }, e: { f: 'g' } } };
objectScan(['a.*.f'], { joined: true })(haystack);
// => [ 'a.e.f' ]
```
## Features
- Input traversed exactly once during search
- Dependency free, small in size and very performant
- Separate Object and Array matching
- Wildcard and Regex matching
- Arbitrary depth matching
- Or-clause Syntax
- Exclusion Matching
- Full support for escaping
- Traversal in "delete-safe" order
- Recursion free implementation
- Search syntax validated
- Lots of tests and examples
## Matching
A needle expression specifies one or more paths to an element (or a set of elements) in a JSON structure. Paths use the dot notation:
```txt
store.book[0].title
```
### Array
Rectangular brackets for array path matching.
_Examples_:
<details><summary> <code>['[2]']</code> <em>(exact in array)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = [0, 1, 2, 3, 4];
objectScan(['[2]'], { joined: true })(haystack);
// => [ '[2]' ]
```
</details>
<details><summary> <code>['[1]']</code> <em>(no match in object)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { 0: 'a', 1: 'b', 2: 'c' };
objectScan(['[1]'], { joined: true })(haystack);
// => []
```
</details>
### Object
Property name for object property matching.
_Examples_:
<details><summary> <code>['foo']</code> <em>(exact in object)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { foo: 0, bar: 1 };
objectScan(['foo'], { joined: true })(haystack);
// => [ 'foo' ]
```
</details>
<details><summary> <code>['1']</code> <em>(no match in array)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = [0, 1, 2, 3, 4];
objectScan(['1'], { joined: true })(haystack);
// => []
```
</details>
### Wildcard
The following characters have special meaning when not escaped:
- `*`: Match zero or more character
- `+`: Match one or more character
- `?`: Match exactly one character
- `\`: Escape the subsequent character
Wildcards can be used with Array and Object selector.
_Examples_:
<details><summary> <code>['*']</code> <em>(top level)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: 0, c: 1 }, d: 2 };
objectScan(['*'], { joined: true })(haystack);
// => [ 'd', 'a' ]
```
</details>
<details><summary> <code>['[?5]']</code> <em>(two digit ending in five)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = [...Array(30).keys()];
objectScan(['[?5]'], { joined: true })(haystack);
// => [ '[25]', '[15]' ]
```
</details>
<details><summary> <code>['a.+.c']</code> <em>(nested)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: { c: 0 }, d: { f: 0 } } };
objectScan(['a.+.c'], { joined: true })(haystack);
// => [ 'a.b.c' ]
```
</details>
<details><summary> <code>['a.\\+.c']</code> <em>(escaped)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: { c: 0 }, '+': { c: 0 } } };
objectScan(['a.\\+.c'], { joined: true })(haystack);
// => [ 'a.\\+.c' ]
```
</details>
### Regex
Regex are defined by using parentheses.
Can be used with Array and Object selector.
_Examples_:
<details><summary> <code>['(^foo)']</code> <em>(starting with `foo`)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { foo: 0, foobar: 1, bar: 2 };
objectScan(['(^foo)'], { joined: true })(haystack);
// => [ 'foobar', 'foo' ]
```
</details>
<details><summary> <code>['[(5)]']</code> <em>(containing `5`)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = [...Array(20).keys()];
objectScan(['[(5)]'], { joined: true })(haystack);
// => [ '[15]', '[5]' ]
```
</details>
<details><summary> <code>['[(^[01]$)]']</code> <em>(`[0]` and `[1]`)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = ['a', 'b', 'c', 'd'];
objectScan(['[(^[01]$)]'], { joined: true })(haystack);
// => [ '[1]', '[0]' ]
```
</details>
<details><summary> <code>['[(^[^01]$)]']</code> <em>(other than `[0]` and `[1]`)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = ['a', 'b', 'c', 'd'];
objectScan(['[(^[^01]$)]'], { joined: true })(haystack);
// => [ '[3]', '[2]' ]
```
</details>
<details><summary> <code>['[*]', '[!(^[01]$)]']</code> <em>(match all and exclude `[0]` and `[1]`)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = ['a', 'b', 'c', 'd'];
objectScan(['[*]', '[!(^[01]$)]'], { joined: true })(haystack);
// => [ '[3]', '[2]' ]
```
</details>
### Arbitrary Depth
There are two types of arbitrary depth matching:
- `**`: Matches zero or more nestings
- `++`: Matches one or more nestings
Recursions can be combined with a regex by appending the regex.
_Examples_:
<details><summary> <code>['a.**']</code> <em>(zero or more nestings under `a`)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: 0, c: 0 } };
objectScan(['a.**'], { joined: true })(haystack);
// => [ 'a.c', 'a.b', 'a' ]
```
</details>
<details><summary> <code>['a.++']</code> <em>(one or more nestings under `a`)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: 0, c: 0 } };
objectScan(['a.++'], { joined: true })(haystack);
// => [ 'a.c', 'a.b' ]
```
</details>
<details><summary> <code>['**(1)']</code> <em>(all containing `1` at every level)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { 1: { 1: ['c', 'd'] }, 510: 'e', foo: { 1: 'f' } };
objectScan(['**(1)'], { joined: true })(haystack);
// => [ '510', '1.1[1]', '1.1', '1' ]
```
</details>
### Or Clause
Or Clauses are defined by using curley brackets.
Can be used with Array and Object selector.
_Examples_:
<details><summary> <code>['[{0,1}]']</code> <em>(`[0]` and `[1]`)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = ['a', 'b', 'c', 'd'];
objectScan(['[{0,1}]'], { joined: true })(haystack);
// => [ '[1]', '[0]' ]
```
</details>
<details><summary> <code>['{a,d}.{b,f}']</code> <em>(`a.b`, `a.f`, `d.b` and `d.f`)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: 0, c: 1 }, d: { e: 2, f: 3 } };
objectScan(['{a,d}.{b,f}'], { joined: true })(haystack);
// => [ 'd.f', 'a.b' ]
```
</details>
### Exclusion
To exclude a path, use exclamation mark.
_Examples_:
<details><summary> <code>['{a,b},!a']</code> <em>(only `b`)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: 0, b: 1 };
objectScan(['{a,b},!a'], {
joined: true,
strict: false
})(haystack);
// => [ 'b' ]
```
</details>
<details><summary> <code>['**,!**.a']</code> <em>(all except ending in `a`)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: 0, b: { a: 1, c: 2 } };
objectScan(['**,!**.a'], { joined: true })(haystack);
// => [ 'b.c', 'b' ]
```
</details>
### Escaping
The following characters are considered special and need to
be escaped using `\`, if they should be matched in a key:<br>
`[`, `]`, `{`, `}`, `(`, `)`, `,`, `.`, `!`, `?`, `*`, `+` and `\`.
_Examples:_
<details><summary> <code>['\\[1\\]']</code> <em>(special object key)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { '[1]': 0 };
objectScan(['\\[1\\]'], { joined: true })(haystack);
// => [ '\\[1\\]' ]
```
</details>
## Options
Signature of all callbacks is
Fn({ key, value, ... })
where:
- `key`: key that callback is invoked for (respects `joined` option).
- `value`: value for key.
- `entry`: entry consisting of [`key`, `value`].
- `property`: current parent property.
- `parent`: current parent.
- `parents`: array of form `[parent, grandparent, ...]`.
- `isMatch`: true iff last targeting needle exists and is non-excluding.
- `matchedBy`: all non-excluding needles targeting key.
- `excludedBy`: all excluding needles targeting key.
- `traversedBy`: all needles involved in traversing key.
- `isCircular`: true iff `value` contained in `parents`
- `isLeaf`: true iff `value` can not be traversed
- `getKey`: function that returns `key`
- `getValue`: function that returns `value`
- `getEntry`: function that returns `entry`
- `getProperty`: function that returns `property`
- `getParent`: function that returns `parent`
- `getParents`: function that returns `parents`
- `getIsMatch`: function that returns `isMatch`
- `getMatchedBy`: function that returns `matchedBy`
- `getExcludedBy`: function that returns `excludedBy`
- `getTraversedBy`: function that returns `traversedBy`
- `getIsCircular`: function that returns `isCircular`
- `getIsLeaf`: function that returns `isLeaf`
- `context`: as passed into the search
Notes on Performance:
- Arguments backed by getters use [Functions Getter](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/get)
and should be accessed via [destructuring](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment#Unpacking_fields_from_objects_passed_as_function_parameter) to prevent redundant computation.
- Getters should be used to improve performance for conditional access. E.g. `if (isMatch) { getParents() ... }`.
- For performance reasons, the same object is passed to all callbacks.
#### filterFn
Type: `function`<br>
Default: `undefined`
When defined, this callback is invoked for every match. If `false`
is returned, the current key is excluded from the result.
The return value of this callback has no effect when a search context is provided.
Can be used to do processing as matching keys are traversed.
Invoked in same order as matches would appear in result.
This method is conceptually similar to
[Array.filter()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter).
_Examples_:
<details><summary> <code>['**']</code> <em>(filter function)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: 0, b: 'bar' };
objectScan(['**'], {
joined: true,
filterFn: ({ value }) => typeof value === 'string'
})(haystack);
// => [ 'b' ]
```
</details>
#### breakFn
Type: `function`<br>
Default: `undefined`
When defined, this callback is invoked for every key that is traversed by
the search. If `true` is returned, all keys nested under the current key are
skipped in the search and from the final result.
Note that `breakFn` is invoked before the corresponding `filterFn` might be invoked.
_Examples_:
<details><summary> <code>['**']</code> <em>(break function)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: { c: 0 } } };
objectScan(['**'], {
joined: true,
breakFn: ({ key }) => key === 'a.b'
})(haystack);
// => [ 'a.b', 'a' ]
```
</details>
#### compareFn
Type: `function`<br>
Default: `undefined`
When defined, this function is used as a comparator to determine the traversal order of any `object` keys.
This works together with the `reverse` option.
_Examples_:
<details><summary> <code>['**']</code> <em>(simple sort)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: 0, c: 1, b: 2 };
objectScan(['**'], {
joined: true,
compareFn: (k1, k2) => k1.localeCompare(k2),
reverse: false
})(haystack);
// => [ 'a', 'b', 'c' ]
```
</details>
#### reverse
Type: `boolean`<br>
Default: `true`
When set to `true`, the scan is performed in reverse order. This means `breakFn` is executed in _reverse post-order_ and
`filterFn` in _reverse pre-order_. Otherwise `breakFn` is executed in _pre-order_ and `filterFn` in _post-order_.
When `reverse` is `true` the scan is _delete-safe_. I.e. `property` can be deleted / spliced from `parent` object / array in `filterFn`.
_Examples_:
<details><summary> <code>['**']</code> <em>(breakFn, reverse true)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { f: { b: { a: {}, d: { c: {}, e: {} } }, g: { i: { h: {} } } } };
objectScan(['**'], {
breakFn: ({ isMatch, property, context }) => { if (isMatch) { context.push(property); } },
reverse: true
})(haystack, []);
// => [ 'f', 'g', 'i', 'h', 'b', 'd', 'e', 'c', 'a' ]
```
</details>
<details><summary> <code>['**']</code> <em>(filterFn, reverse true)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { f: { b: { a: {}, d: { c: {}, e: {} } }, g: { i: { h: {} } } } };
objectScan(['**'], {
filterFn: ({ property, context }) => { context.push(property); },
reverse: true
})(haystack, []);
// => [ 'h', 'i', 'g', 'e', 'c', 'd', 'a', 'b', 'f' ]
```
</details>
<details><summary> <code>['**']</code> <em>(breakFn, reverse false)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { f: { b: { a: {}, d: { c: {}, e: {} } }, g: { i: { h: {} } } } };
objectScan(['**'], {
breakFn: ({ isMatch, property, context }) => { if (isMatch) { context.push(property); } },
reverse: false
})(haystack, []);
// => [ 'f', 'b', 'a', 'd', 'c', 'e', 'g', 'i', 'h' ]
```
</details>
<details><summary> <code>['**']</code> <em>(filterFn, reverse false)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { f: { b: { a: {}, d: { c: {}, e: {} } }, g: { i: { h: {} } } } };
objectScan(['**'], {
filterFn: ({ property, context }) => { context.push(property); },
reverse: false
})(haystack, []);
// => [ 'a', 'c', 'e', 'd', 'b', 'h', 'i', 'g', 'f' ]
```
</details>
#### abort
Type: `boolean`<br>
Default: `false`
When set to `true` the scan immediately returns after the first match.
_Examples_:
<details><summary> <code>['a', 'b']</code> <em>(only return first property)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: 0, b: 1 };
objectScan(['a', 'b'], {
rtn: 'property',
abort: true
})(haystack);
// => 'b'
```
</details>
<details><summary> <code>['[0]', '[1]']</code> <em>(abort changes count)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = ['a', 'b'];
objectScan(['[0]', '[1]'], {
rtn: 'count',
abort: true
})(haystack);
// => 1
```
</details>
#### rtn
Type: `string`<br>
Default: _dynamic_
Defaults to `key` when search context is _undefined_ and to `context` otherwise.
Can be explicitly set as:
- `context`: search context is returned
- `key`: matched keys are returned
- `value`: matched values are returned
- `entry`: matched entries are returned
- `property`: matched properties are returned
- `parent`: matched parent are returned
- `parents`: matched parents are returned
- `bool`: returns _true_ iff a match is found
- `count`: returns the match count
When **abort** is set to `true` and the result would be a list, the first match or _undefined_ is returned.
_Examples_:
<details><summary> <code>['[*]']</code> <em>(return values)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = ['a', 'b', 'c'];
objectScan(['[*]'], { rtn: 'value' })(haystack);
// => [ 'c', 'b', 'a' ]
```
</details>
<details><summary> <code>['foo[*]']</code> <em>(return entries)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { foo: ['bar'] };
objectScan(['foo[*]'], { rtn: 'entry' })(haystack);
// => [ [ [ 'foo', 0 ], 'bar' ] ]
```
</details>
<details><summary> <code>['a.b.c', 'a']</code> <em>(return properties)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: { c: 0 } } };
objectScan(['a.b.c', 'a'], { rtn: 'property' })(haystack);
// => [ 'c', 'a' ]
```
</details>
<details><summary> <code>['a.b', 'a.c']</code> <em>(checks for any match, full scan)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: 0, c: 1 } };
objectScan(['a.b', 'a.c'], { rtn: 'bool' })(haystack);
// => true
```
</details>
<details><summary> <code>['**']</code> <em>(return not provided context)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: 0 };
objectScan(['**'], { rtn: 'context' })(haystack);
// => undefined
```
</details>
<details><summary> <code>['a.b.{c,d}']</code> <em>(return keys with context passed)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: { c: 0, d: 1 } } };
objectScan(['a.b.{c,d}'], { rtn: 'key' })(haystack, []);
// => [ [ 'a', 'b', 'd' ], [ 'a', 'b', 'c' ] ]
```
</details>
#### joined
Type: `boolean`<br>
Default: `false`
Keys are returned as a string when set to `true` instead of as a list.
Setting this option to `true` will negatively impact performance.
Note that [_.get](https://lodash.com/docs/#get) and [_.set](https://lodash.com/docs/#set) fully support lists.
_Examples_:
<details><summary> <code>['[*]', '[*].foo']</code> <em>(joined)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = [0, 1, { foo: 'bar' }];
objectScan(['[*]', '[*].foo'], { joined: true })(haystack);
// => [ '[2].foo', '[2]', '[1]', '[0]' ]
```
</details>
<details><summary> <code>['[*]', '[*].foo']</code> <em>(not joined)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = [0, 1, { foo: 'bar' }];
objectScan(['[*]', '[*].foo'])(haystack);
// => [ [ 2, 'foo' ], [ 2 ], [ 1 ], [ 0 ] ]
```
</details>
#### useArraySelector
Type: `boolean`<br>
Default: `true`
When set to `false`, no array selectors should be used in any needles and arrays are automatically traversed.
Note that the results still include the array selectors.
_Examples_:
<details><summary> <code>['a', 'b.d']</code> <em>(automatic array traversal)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = [{ a: 0 }, { b: [{ c: 1 }, { d: 2 }] }];
objectScan(['a', 'b.d'], {
joined: true,
useArraySelector: false
})(haystack);
// => [ '[1].b[1].d', '[0].a' ]
```
</details>
<details><summary> <code>['']</code> <em>(top level array matching)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = [{ a: 0 }, { b: 1 }];
objectScan([''], {
joined: true,
useArraySelector: false
})(haystack);
// => [ '[1]', '[0]' ]
```
</details>
#### strict
Type: `boolean`<br>
Default: `true`
When set to `true`, errors are thrown when:
- a path is identical to a previous path
- a path invalidates a previous path
- a path contains consecutive recursions
_Examples_:
<details><summary> <code>['a.b', 'a.b']</code> <em>(identical)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = [];
objectScan(['a.b', 'a.b'], { joined: true })(haystack);
// => 'Error: Redundant Needle Target: "a.b" vs "a.b"'
```
</details>
<details><summary> <code>['a.{b,b}']</code> <em>(identical, same needle)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = [];
objectScan(['a.{b,b}'], { joined: true })(haystack);
// => 'Error: Redundant Needle Target: "a.{b,b}" vs "a.{b,b}"'
```
</details>
<details><summary> <code>['a.b', 'a.**']</code> <em>(invalidates previous)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = [];
objectScan(['a.b', 'a.**'], { joined: true })(haystack);
// => 'Error: Needle Target Invalidated: "a.b" by "a.**"'
```
</details>
<details><summary> <code>['**.!**']</code> <em>(consecutive recursion)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = [];
objectScan(['**.!**'], { joined: true })(haystack);
// => 'Error: Redundant Recursion: "**.!**"'
```
</details>
### Search Context
A context can be passed into a search invocation as a second parameter. It is available in all callbacks
and can be used to manage state across a search invocation without having to recompile the search.
By default all matched keys are returned from a search invocation.
However, when it is not _undefined_, the context is returned instead.
_Examples_:
<details><summary> <code>['**.{c,d,e}']</code> <em>(sum values)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: { c: 2, d: 11 }, e: 7 } };
objectScan(['**.{c,d,e}'], {
joined: true,
filterFn: ({ value, context }) => { context.sum += value; }
})(haystack, { sum: 0 });
// => { sum: 20 }
```
</details>
## Examples
More extensive examples can be found in the tests.
<details><summary> <code>['a.*.f']</code> <em>(nested)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: { c: 'd' }, e: { f: 'g' }, h: ['i', 'j'] }, k: 'l' };
objectScan(['a.*.f'], { joined: true })(haystack);
// => [ 'a.e.f' ]
```
</details>
<details><summary> <code>['*.*.*']</code> <em>(multiple nested)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: { c: 'd' }, e: { f: 'g' }, h: ['i', 'j'] }, k: 'l' };
objectScan(['*.*.*'], { joined: true })(haystack);
// => [ 'a.e.f', 'a.b.c' ]
```
</details>
<details><summary> <code>['a.*.{c,f}']</code> <em>(or filter)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: { c: 'd' }, e: { f: 'g' }, h: ['i', 'j'] }, k: 'l' };
objectScan(['a.*.{c,f}'], { joined: true })(haystack);
// => [ 'a.e.f', 'a.b.c' ]
```
</details>
<details><summary> <code>['a.*.{c,f}']</code> <em>(or filter, not joined)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: { c: 'd' }, e: { f: 'g' }, h: ['i', 'j'] }, k: 'l' };
objectScan(['a.*.{c,f}'])(haystack);
// => [ [ 'a', 'e', 'f' ], [ 'a', 'b', 'c' ] ]
```
</details>
<details><summary> <code>['*.*[*]']</code> <em>(list filter)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: { c: 'd' }, e: { f: 'g' }, h: ['i', 'j'] }, k: 'l' };
objectScan(['*.*[*]'], { joined: true })(haystack);
// => [ 'a.h[1]', 'a.h[0]' ]
```
</details>
<details><summary> <code>['*[*]']</code> <em>(list filter, unmatched)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: { c: 'd' }, e: { f: 'g' }, h: ['i', 'j'] }, k: 'l' };
objectScan(['*[*]'], { joined: true })(haystack);
// => []
```
</details>
<details><summary> <code>['**']</code> <em>(star recursion)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: { c: 'd' }, e: { f: 'g' }, h: ['i', 'j'] }, k: 'l' };
objectScan(['**'], { joined: true })(haystack);
// => [ 'k', 'a.h[1]', 'a.h[0]', 'a.h', 'a.e.f', 'a.e', 'a.b.c', 'a.b', 'a' ]
```
</details>
<details><summary> <code>['++.++']</code> <em>(plus recursion)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: { c: 'd' }, e: { f: 'g' }, h: ['i', 'j'] }, k: 'l' };
objectScan(['++.++'], { joined: true })(haystack);
// => [ 'a.h[1]', 'a.h[0]', 'a.h', 'a.e.f', 'a.e', 'a.b.c', 'a.b' ]
```
</details>
<details><summary> <code>['**.f']</code> <em>(star recursion ending in f)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: { c: 'd' }, e: { f: 'g' }, h: ['i', 'j'] }, k: 'l' };
objectScan(['**.f'], { joined: true })(haystack);
// => [ 'a.e.f' ]
```
</details>
<details><summary> <code>['**[*]']</code> <em>(star recursion ending in array)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: { c: 'd' }, e: { f: 'g' }, h: ['i', 'j'] }, k: 'l' };
objectScan(['**[*]'], { joined: true })(haystack);
// => [ 'a.h[1]', 'a.h[0]' ]
```
</details>
<details><summary> <code>['a.*,!a.e']</code> <em>(exclusion filter)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: { c: 'd' }, e: { f: 'g' }, h: ['i', 'j'] }, k: 'l' };
objectScan(['a.*,!a.e'], { joined: true })(haystack);
// => [ 'a.h', 'a.b' ]
```
</details>
<details><summary> <code>['**.(^[bc]$)']</code> <em>(regex matching)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { a: { b: { c: 'd' }, e: { f: 'g' }, h: ['i', 'j'] }, k: 'l' };
objectScan(['**.(^[bc]$)'], { joined: true })(haystack);
// => [ 'a.b.c', 'a.b' ]
```
</details>
## Edge Cases
Top level object(s) are matched by the empty needle `''`. This is useful for matching objects nested in arrays by setting `useArraySelector` to `false`.
To match the actual empty string as a key, use `(^$)`.
Note that the empty string does not work to match top level objects with
[_.get](https://lodash.com/docs/#get) or [_.set](https://lodash.com/docs/#set).
_Examples_:
<details><summary> <code>['']</code> <em>(match top level objects in array)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = [{}, {}];
objectScan([''], {
joined: true,
useArraySelector: false
})(haystack);
// => [ '[1]', '[0]' ]
```
</details>
<details><summary> <code>['']</code> <em>(match top level object)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = {};
objectScan([''], { joined: true })(haystack);
// => [ '' ]
```
</details>
<details><summary> <code>['**.(^$)']</code> <em>(match empty string keys)</em> </summary>
<!-- eslint-disable no-undef -->
```js
const haystack = { '': 0, a: { '': 1 } };
objectScan(['**.(^$)'])(haystack);
// => [ [ 'a', '' ], [ '' ] ]
```
</details>
## Internals
Conceptually this package works as follows:
1. During initialization the needles are parsed and built into a search tree.
Various information is pre-computed and stored for every node.
Finally the search function is returned.
2. When the search function is invoked, the input is traversed simultaneously with
the relevant nodes of the search tree. Processing multiple search tree branches
in parallel allows for a single traversal of the input.
Having a separate initialization stage allows for a performant search and
significant speed ups when applying the same search to different input.
@@ -0,0 +1,248 @@
"use strict";
/* compile needles to hierarchical map object */
const parser = require('./parser');
const iterator = require('../generic/iterator');
const traverser = require('../generic/traverser');
const {
defineProperty
} = require('../generic/helper');
const {
Wildcard
} = require('./wildcard');
const LEAF = Symbol('leaf');
const markLeaf = (input, match, readonly) => defineProperty(input, LEAF, match, readonly);
const isLeaf = input => LEAF in input;
const isMatch = input => input !== undefined && input[LEAF] === true;
module.exports.isLeaf = isLeaf;
module.exports.isMatch = isMatch;
const HAS_MATCHES = Symbol('has-matches');
const setHasMatches = input => defineProperty(input, HAS_MATCHES, true);
const hasMatches = input => input[HAS_MATCHES] === true;
module.exports.hasMatches = hasMatches;
const merge = (input, symbol, value) => {
if (input[symbol] === undefined) {
defineProperty(input, symbol, []);
}
if (!input[symbol].includes(value)) {
input[symbol].push(value);
}
};
const LEAF_NEEDLES = Symbol('leaf-needles');
const addLeafNeedle = (input, needle) => merge(input, LEAF_NEEDLES, needle);
const getLeafNeedles = input => input[LEAF_NEEDLES] || [];
module.exports.getLeafNeedles = getLeafNeedles;
const LEAF_NEEDLES_EXCLUDE = Symbol('leaf-needles-exclude');
const addLeafNeedleExclude = (input, needle) => merge(input, LEAF_NEEDLES_EXCLUDE, needle);
const getLeafNeedlesExclude = input => input[LEAF_NEEDLES_EXCLUDE] || [];
module.exports.getLeafNeedlesExclude = getLeafNeedlesExclude;
const LEAF_NEEDLES_MATCH = Symbol('leaf-needles-match');
const addLeafNeedleMatch = (input, needle) => merge(input, LEAF_NEEDLES_MATCH, needle);
const getLeafNeedlesMatch = input => input[LEAF_NEEDLES_MATCH] || [];
module.exports.getLeafNeedlesMatch = getLeafNeedlesMatch;
const NEEDLES = Symbol('needles');
const addNeedle = (input, needle) => merge(input, NEEDLES, needle);
const getNeedles = input => input[NEEDLES];
module.exports.getNeedles = getNeedles;
const INDEX = Symbol('index');
const setIndex = (input, index, readonly) => defineProperty(input, INDEX, index, readonly);
const getIndex = input => input[INDEX];
module.exports.getIndex = getIndex;
const WILDCARD = Symbol('wildcard');
const setWildcard = (input, wildcard) => defineProperty(input, WILDCARD, wildcard);
const getWildcard = input => input[WILDCARD];
module.exports.getWildcard = getWildcard;
const VALUES = Symbol('values');
const setValues = (input, entries) => defineProperty(input, VALUES, entries);
const getValues = input => input[VALUES];
module.exports.getValues = getValues;
module.exports.excludedBy = searches => Array.from(new Set([].concat(...searches.map(e => getLeafNeedlesExclude(e)))));
module.exports.matchedBy = searches => Array.from(new Set([].concat(...searches.map(e => getLeafNeedlesMatch(e)))));
module.exports.traversedBy = searches => Array.from(new Set([].concat(...searches.map(e => getNeedles(e)))));
module.exports.isLastLeafMatch = searches => {
let maxLeafIndex = Number.MIN_SAFE_INTEGER;
let maxLeaf = null;
for (let idx = 0, len = searches.length; idx < len; idx += 1) {
const s = searches[idx];
const index = getIndex(s);
if (index !== undefined && index > maxLeafIndex) {
maxLeafIndex = index;
maxLeaf = s;
}
}
return maxLeaf !== null && isMatch(maxLeaf);
};
const iterate = (tower, needle, tree, {
onAdd,
onFin
}) => {
const stack = [[[tower, null]]];
const wildcards = [];
let excluded = false;
iterator.iterate(tree, (type, wc) => {
if (type === 'RM') {
if (wc.excluded === true) {
excluded = false;
}
stack.pop();
wildcards.pop();
} else if (type === 'ADD') {
if (wc.excluded === true) {
if (excluded) {
throw new Error(`Redundant Exclusion: "${needle}"`);
}
excluded = true;
}
const toAdd = [];
const wcParent = wildcards[wildcards.length - 1];
stack[stack.length - 1].forEach(([cur]) => onAdd(cur, wc, wcParent, e => toAdd.push([e, cur])));
stack.push(toAdd);
wildcards.push(wc);
} else {
stack[stack.length - 1].filter(([cur]) => cur !== tower).forEach(([cur, parent]) => onFin(cur, wc[wc.length - 1], parent, excluded));
}
});
};
const applyNeedle = (tower, needle, tree, strict, ctx) => {
iterate(tower, needle, tree, {
onAdd: (cur, wc, wcParent, next) => {
addNeedle(cur, needle);
const redundantRecursion = wcParent !== undefined && wc.isStarRec && wc.value === wcParent.value;
if (redundantRecursion && strict) {
throw new Error(`Redundant Recursion: "${needle}"`);
}
if (!redundantRecursion) {
if (cur[wc] === undefined) {
const child = {}; // eslint-disable-next-line no-param-reassign
cur[wc] = child;
setWildcard(child, wc);
}
next(cur[wc]);
}
if (wc.isStarRec) {
next(cur);
}
},
onFin: (cur, wc, parent, excluded) => {
if (strict) {
if (wc.isSimpleStarRec) {
const unnecessary = Object.keys(parent).filter(k => !['**', ''].includes(k));
if (unnecessary.length !== 0) {
throw new Error(`Needle Target Invalidated: "${parent[unnecessary[0]][NEEDLES][0]}" by "${needle}"`);
}
}
}
addNeedle(cur, needle);
if (strict && LEAF_NEEDLES in cur) {
throw new Error(`Redundant Needle Target: "${cur[LEAF_NEEDLES][0]}" vs "${needle}"`);
}
addLeafNeedle(cur, needle, strict);
if (excluded) {
addLeafNeedleExclude(cur, needle);
} else {
addLeafNeedleMatch(cur, needle);
}
markLeaf(cur, !excluded, strict);
setIndex(cur, ctx.index, strict);
ctx.index += 1;
}
});
};
const finalizeTower = tower => {
const matches = [];
let lastDepth = -1;
traverser.traverse(tower, (type, obj, depth) => {
if (type === 'EXIT') {
const isUp = lastDepth === depth + 1;
if (isUp && matches[lastDepth] === true || isMatch(obj)) {
matches[depth] = true;
setHasMatches(obj);
}
if (isUp) {
matches[lastDepth] = false;
}
setValues(obj, Object.entries(obj).filter(([k]) => k !== '').map(e => e[1]).reverse());
lastDepth = depth;
}
});
};
module.exports.compile = (needles, strict = true, useArraySelector = true) => {
const tower = {};
const ctx = {
index: 0
};
for (let idx = 0; idx < needles.length; idx += 1) {
const needle = needles[idx];
const tree = [parser.parse(needle, useArraySelector)];
applyNeedle(tower, needle, tree, strict, ctx);
}
setWildcard(tower, new Wildcard('*', false));
finalizeTower(tower);
return tower;
};
@@ -0,0 +1,36 @@
"use strict";
module.exports = (kwargs, ctx) => {
if (ctx.rtn === 'context') {
return {
onMatch: () => {},
finish: () => ctx.context
};
}
if (ctx.rtn === 'bool') {
let result = false;
return {
onMatch: () => {
result = true;
},
finish: () => result
};
}
if (ctx.rtn === 'count') {
let result = 0;
return {
onMatch: () => {
result += 1;
},
finish: () => result
};
}
const result = [];
return {
onMatch: () => result.push(kwargs[ctx.rtn]),
finish: () => ctx.abort ? result[0] : result
};
};
@@ -0,0 +1,209 @@
"use strict";
const assert = require('assert');
const compiler = require('./compiler');
const Result = require('./find-result');
const {
toPath
} = require('../generic/helper');
const formatPath = (input, ctx) => ctx.joined ? toPath(input) : [...input];
module.exports = (haystack_, searches_, ctx) => {
const stack = [false, searches_, null, 0];
const path = [];
const parents = [];
let depth;
let segment;
let searches;
let isMatch;
let haystack = haystack_;
const kwargs = {
getKey: () => formatPath(path, ctx),
get key() {
return kwargs.getKey();
},
getValue: () => haystack,
get value() {
return kwargs.getValue();
},
getEntry: () => [formatPath(path, ctx), haystack],
get entry() {
return kwargs.getEntry();
},
getIsMatch: () => isMatch,
get isMatch() {
return kwargs.getIsMatch();
},
getMatchedBy: () => compiler.matchedBy(searches),
get matchedBy() {
return kwargs.getMatchedBy();
},
getExcludedBy: () => compiler.excludedBy(searches),
get excludedBy() {
return kwargs.getExcludedBy();
},
getTraversedBy: () => compiler.traversedBy(searches),
get traversedBy() {
return kwargs.getTraversedBy();
},
getProperty: () => path[path.length - 1],
get property() {
return kwargs.getProperty();
},
getParent: () => parents[parents.length - 1],
get parent() {
return kwargs.getParent();
},
getParents: () => [...parents].reverse(),
get parents() {
return kwargs.getParents();
},
getIsCircular: () => parents.includes(haystack),
get isCircular() {
return kwargs.getIsCircular();
},
getIsLeaf: () => !(haystack instanceof Object),
get isLeaf() {
return kwargs.getIsLeaf();
},
context: ctx.context
};
const result = Result(kwargs, ctx);
do {
depth = stack.pop();
segment = stack.pop();
searches = stack.pop();
isMatch = stack.pop();
const diff = path.length - depth;
for (let idx = 0; idx < diff; idx += 1) {
parents.pop();
path.pop();
}
if (diff === -1) {
parents.push(haystack);
path.push(segment);
haystack = haystack[segment];
} else if (segment !== null) {
path[path.length - 1] = segment;
haystack = parents[parents.length - 1][segment];
} else {
haystack = haystack_;
}
if (isMatch) {
if (ctx.filterFn === undefined || ctx.filterFn(kwargs) !== false) {
result.onMatch();
if (ctx.abort) {
return result.finish();
}
} // eslint-disable-next-line no-continue
continue;
}
if (!searches.some(s => compiler.hasMatches(s))) {
// eslint-disable-next-line no-continue
continue;
}
if (ctx.useArraySelector === false && Array.isArray(haystack)) {
if (ctx.breakFn === undefined || ctx.breakFn(kwargs) !== true) {
for (let idx = 0, len = haystack.length; idx < len; idx += 1) {
stack.push(false, searches, idx, depth + 1);
}
} // eslint-disable-next-line no-continue
continue;
}
const searchesIn = searches;
if (compiler.isLastLeafMatch(searches)) {
stack.push(true, searches, segment, depth);
isMatch = true;
}
if ('' in searches[0] && path.every(p => Number.isInteger(p))) {
assert(searches.length === 1);
stack.push(true, [searches[0]['']], segment, depth);
isMatch = true;
searches = [searches[0]['']];
}
if ((ctx.breakFn === undefined || ctx.breakFn(kwargs) !== true) && haystack instanceof Object) {
const isArray = Array.isArray(haystack);
const keys = Object.keys(haystack);
if (!isArray && ctx.compareFn) {
keys.sort(ctx.compareFn);
}
if (!ctx.reverse) {
keys.reverse();
}
for (let kIdx = 0, kLen = keys.length; kIdx < kLen; kIdx += 1) {
const key = keys[kIdx];
const searchesOut = [];
for (let sIdx = 0, sLen = searchesIn.length; sIdx !== sLen; sIdx += 1) {
const search = searchesIn[sIdx];
const wildcard = compiler.getWildcard(search);
if (wildcard.isRec && wildcard.anyMatch(key)) {
searchesOut.push(search);
}
const values = compiler.getValues(search);
let eIdx = values.length; // eslint-disable-next-line no-plusplus
while (eIdx--) {
const value = values[eIdx];
if (compiler.getWildcard(value).typeMatch(key, isArray)) {
searchesOut.push(value);
}
}
}
stack.push(false, searchesOut, isArray ? Number(key) : key, depth + 1);
}
}
} while (stack.length !== 0);
return result.finish();
};
@@ -0,0 +1,156 @@
"use strict";
const assert = require('assert');
const {
defineProperty
} = require('../generic/helper');
const {
Wildcard
} = require('./wildcard');
const IS_EXCLUDED = Symbol('is-excluded');
const markExcluded = input => defineProperty(input, IS_EXCLUDED, true);
const isExcluded = input => input[IS_EXCLUDED] === true;
const throwError = (msg, input, context = {}) => {
throw new Error(Object.entries(context).reduce((p, [k, v]) => `${p}, ${k} ${v}`, `${msg}: ${input}`));
};
const getSimple = arrOrSet => {
if (Array.isArray(arrOrSet)) {
return arrOrSet.length === 1 ? arrOrSet[0] : arrOrSet;
}
return arrOrSet.size === 1 ? arrOrSet.values().next().value : arrOrSet;
};
module.exports = input => {
let cResult = new Set();
let inArray = false;
let excludeNext = false;
let cursor = 0; // group related
const parentStack = [];
const newChild = asOr => {
if (isExcluded(cResult)) {
assert(excludeNext === false);
excludeNext = true;
}
parentStack.push(cResult);
cResult = asOr ? new Set() : [];
};
const finishChild = () => {
const parent = parentStack.pop();
const parentIsArray = Array.isArray(parent);
const child = getSimple(cResult);
if (!parentIsArray && child instanceof Set) {
child.forEach(e => parent.add(e));
} else {
parent[parentIsArray ? 'push' : 'add'](child);
}
cResult = parent;
};
newChild(false);
return {
setInArray: (flag, idx) => {
if (inArray === flag) {
throwError(inArray ? 'Bad Array Start' : 'Bad Array Terminator', input, {
char: idx
});
}
inArray = flag;
},
finishElement: (idx, {
err,
fins,
finReq = false
}) => {
const isFinished = cursor === idx;
if (isFinished && !fins.includes(input[idx - 1] || null)) {
throwError(err, input, {
char: idx
});
}
if (!isFinished) {
if (finReq) {
throwError(err, input, {
char: idx
});
}
const ele = input.slice(cursor, idx);
if (inArray && !(/^[?*+\d]+$/.test(ele) || ele.startsWith('(') && ele.endsWith(')'))) {
throwError('Bad Array Selector', input, {
selector: ele
});
}
cResult.push(new Wildcard(inArray ? `[${ele}]` : ele, excludeNext));
excludeNext = false;
}
cursor = idx + 1;
},
startExclusion: idx => {
if (excludeNext !== false) {
throwError('Redundant Exclusion', input, {
char: idx
});
}
excludeNext = true;
},
startGroup: () => {
newChild(true);
if (excludeNext) {
markExcluded(cResult);
excludeNext = false;
}
newChild(false);
},
newGroupElement: () => {
finishChild();
newChild(false);
},
finishGroup: idx => {
if (parentStack.length < 2) {
throwError('Unexpected Group Terminator', input, {
char: idx
});
}
finishChild();
finishChild();
},
finalizeResult: () => {
finishChild();
assert(excludeNext === false);
if (parentStack.length !== 0) {
throwError('Non Terminated Group', input);
}
if (inArray) {
throwError('Non Terminated Array', input);
}
return getSimple(cResult);
}
};
};
@@ -0,0 +1,135 @@
"use strict";
const {
Wildcard
} = require('./wildcard');
const Result = require('./parser-result');
const throwError = (msg, input, context = {}) => {
throw new Error(Object.entries(context).reduce((p, [k, v]) => `${p}, ${k} ${v}`, `${msg}: ${input}`));
};
module.exports.parse = (input, useArraySelector = true) => {
if (input === '') {
return new Wildcard('', false);
}
const result = Result(input);
const inputLength = input.length;
let escaped = false;
let bracketDepth = 0;
for (let idx = 0; idx < inputLength; idx += 1) {
const char = input[idx];
if (escaped === false && bracketDepth === 0) {
switch (char) {
case '.':
result.finishElement(idx, {
err: 'Bad Path Separator',
fins: [']', '}']
});
break;
case '[':
if (useArraySelector === false) {
throwError('Forbidden Array Selector', input, {
char: idx
});
}
result.finishElement(idx, {
err: 'Bad Array Start',
fins: [null, '!', '{', ',', '}', ']']
});
result.setInArray(true, idx);
break;
case ']':
result.finishElement(idx, {
err: 'Bad Array Terminator',
fins: ['}']
});
result.setInArray(false, idx);
break;
case '{':
result.finishElement(idx, {
err: 'Bad Group Start',
fins: [null, '!', '.', '[', '{', ','],
finReq: true
});
result.startGroup();
break;
case ',':
result.finishElement(idx, {
err: 'Bad Group Separator',
fins: [']', '}']
});
result.newGroupElement();
break;
case '}':
result.finishElement(idx, {
err: 'Bad Group Terminator',
fins: [']', '}']
});
result.finishGroup(idx);
break;
case '!':
result.finishElement(idx, {
err: 'Bad Exclusion',
fins: [null, '.', ',', '{', '['],
finReq: true
});
result.startExclusion(idx);
break;
default:
break;
}
}
if (escaped === false) {
switch (char) {
case '(':
bracketDepth += 1;
break;
case ')':
if (bracketDepth === 0) {
throwError('Unexpected Parentheses', input, {
char: idx
});
}
bracketDepth -= 1;
break;
default:
break;
}
}
escaped = char === '\\' ? !escaped : false;
}
if (escaped !== false) {
throwError('Dangling Escape', input, {
char: inputLength - 1
});
}
if (bracketDepth !== 0) {
throwError('Unterminated Parentheses', input);
}
result.finishElement(inputLength, {
err: 'Bad Terminator',
fins: [']', '}']
});
return result.finalizeResult();
};
@@ -0,0 +1,108 @@
"use strict";
const {
escapeRegex,
asRegex
} = require('../generic/helper');
const parseWildcard = str => {
let regex = '';
let escaped = false;
for (let idx = 0; idx < str.length; idx += 1) {
const char = str[idx];
if (!escaped && char === '\\') {
escaped = true;
} else if (!escaped && char === '*') {
regex += '.*';
} else if (!escaped && char === '+') {
regex += '.+';
} else if (!escaped && char === '?') {
regex += '.';
} else {
regex += escapeRegex(char);
escaped = false;
}
}
return new RegExp(`^${regex}$`);
};
module.exports.parseWildcard = parseWildcard;
const compileWildcard = str => {
if (['**', '++'].includes(str)) {
return asRegex('.*');
}
if ((str.startsWith('**(') || str.startsWith('++(')) && str.endsWith(')')) {
return asRegex(str.slice(3, -1));
}
if (str.startsWith('[(') && str.endsWith(')]')) {
return asRegex(str.slice(2, -2));
}
if (str.startsWith('(') && str.endsWith(')')) {
return asRegex(str.slice(1, -1));
}
if (str.startsWith('[') && str.endsWith(']')) {
return parseWildcard(str.slice(1, -1));
}
return parseWildcard(str);
};
class Wildcard extends String {
constructor(value, excluded) {
super(value);
this.value = value;
this.excluded = excluded;
this.regex = compileWildcard(value);
this.isArrayTarget = value.startsWith('[') && value.endsWith(']');
this.isSimpleStarRec = value === '**';
this.isSimplePlusRec = value === '++';
this.isSimpleRec = this.isSimpleStarRec || this.isSimplePlusRec;
this.isRegexStarRec = value.startsWith('**(') && value.endsWith(')');
this.isRegexPlusRec = value.startsWith('++(') && value.endsWith(')');
this.isRegexRec = this.isRegexStarRec || this.isRegexPlusRec;
this.isStarRec = this.isSimpleStarRec || this.isRegexStarRec;
this.isPlusRec = this.isSimplePlusRec || this.isRegexPlusRec;
this.isRec = this.isStarRec || this.isPlusRec;
this.isAnyArrayTarget = value === '[*]';
this.isAnyObjTarget = value === '*';
}
anyMatch(key) {
if (this.isSimpleRec) {
return true;
}
return this.regex.test(key);
}
typeMatch(key, isArray) {
if (this.isSimpleRec) {
return true;
}
if (isArray && this.isAnyArrayTarget) {
return true;
}
if (!isArray && this.isAnyObjTarget) {
return true;
}
if (isArray !== this.isArrayTarget && !this.isRec) {
return false;
}
return this.regex.test(key);
}
}
module.exports.Wildcard = Wildcard;
@@ -0,0 +1,24 @@
"use strict";
module.exports.defineProperty = (target, k, v, readonly = true) => Object.defineProperty(target, k, {
value: v,
writable: !readonly
});
const specialChars = /[?!,.*+[\](){}\\]/g;
const escape = input => input.replace(specialChars, '\\$&');
module.exports.escape = escape;
module.exports.escapeRegex = char => char.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&');
module.exports.asRegex = regexStr => {
try {
return new RegExp(regexStr);
} catch (e) {
throw new Error(`Invalid Regex: "${regexStr}"`);
}
};
module.exports.toPath = input => input.reduce((p, c) => `${p}${typeof c === 'number' ? `[${c}]` : `${p ? '.' : ''}${escape(c)}`}`, '');
@@ -0,0 +1,65 @@
"use strict";
const iterate = (tree, cb) => {
const stack = [tree];
const parent = [null];
const count = [];
const depth = [];
const path = [];
let idx = 0;
let inc = true;
while (idx !== -1) {
const e = stack[idx];
if (e instanceof Set) {
stack[idx] = [...e];
stack[idx].or = true;
} else if (Array.isArray(e)) {
if (e.or !== true) {
stack.splice(idx, 1, ...e);
parent.splice(idx, 1, ...new Array(e.length).fill(parent[idx]));
if (parent[idx] !== null) {
depth[parent[idx]] += e.length - 1;
}
} else {
if (count[idx] === undefined) {
count[idx] = 0;
depth[idx] = 0;
} else if (depth[idx] !== 0) {
stack.splice(idx + 1, depth[idx]);
parent.splice(idx + 1, depth[idx]);
depth[idx] = 0;
}
if (count[idx] < e.length) {
stack.splice(idx + 1, 0, e[count[idx]]);
parent.splice(idx + 1, 0, idx);
count[idx] = (count[idx] || 0) + 1;
depth[idx] += 1;
inc = true;
idx += 1;
} else {
count[idx] = 0;
idx -= 1;
}
}
} else if (inc === true) {
path.push(e);
cb('ADD', e);
if (idx === stack.length - 1) {
cb('FIN', path);
inc = false;
} else {
idx += 1;
}
} else {
cb('RM', path.pop());
idx -= 1;
}
}
};
module.exports.iterate = iterate;
@@ -0,0 +1,35 @@
"use strict";
module.exports.traverse = (obj, cb) => {
const stack = [obj];
const counts = [1];
let depth = 0;
let inc = true;
while (stack.length !== 0) {
if (inc === true) {
const cur = stack[stack.length - 1];
cb('ENTER', cur, depth);
const values = Object.values(cur);
if (values.length !== 0) {
stack.push(...values);
depth += 1;
counts[depth] = values.length;
} else {
inc = false;
}
} else {
const cur = stack.pop();
cb('EXIT', cur, depth);
counts[depth] -= 1;
if (counts[depth] === 0) {
counts.pop();
depth -= 1;
} else {
inc = true;
}
}
}
};
+46
View File
@@ -0,0 +1,46 @@
"use strict";
const assert = require('assert');
const compiler = require('./core/compiler');
const find = require('./core/find');
module.exports = (needles, opts = {}) => {
assert(Array.isArray(needles));
assert(opts instanceof Object && !Array.isArray(opts));
if (needles.length === 0) {
return (_, ctx) => ctx === undefined ? [] : ctx;
}
const ctx = {
filterFn: undefined,
breakFn: undefined,
compareFn: undefined,
reverse: true,
abort: false,
rtn: undefined,
joined: false,
useArraySelector: true,
strict: true,
...opts
};
assert(Object.keys(ctx).length === 9, 'Unexpected Option provided!');
assert(['function', 'undefined'].includes(typeof ctx.filterFn));
assert(['function', 'undefined'].includes(typeof ctx.breakFn));
assert(['function', 'undefined'].includes(typeof ctx.compareFn));
assert(typeof ctx.reverse === 'boolean');
assert(typeof ctx.abort === 'boolean');
assert([undefined, 'context', 'key', 'value', 'entry', 'property', 'parent', 'parents', 'bool', 'count'].includes(opts.rtn));
assert(typeof ctx.joined === 'boolean');
assert(typeof ctx.useArraySelector === 'boolean');
assert(typeof ctx.strict === 'boolean');
const search = compiler.compile(needles, ctx.strict, ctx.useArraySelector); // keep separate for performance
return (haystack, context) => find(haystack, [search], {
context,
...ctx,
rtn: ctx.rtn || (context === undefined ? 'key' : 'context')
});
};
+153
View File
@@ -0,0 +1,153 @@
{
"_from": "object-scan@^13.0.0",
"_id": "object-scan@13.9.3",
"_inBundle": false,
"_integrity": "sha512-gb27kO8ovRqt9V3BfPygvV/Dfd5cIk+QEX/V5D85M7kYimkcb9iTyKZLjSaojyPJUk/Ugr7yU36xaKaIVHbMOA==",
"_location": "/object-scan",
"_phantomChildren": {},
"_requested": {
"type": "range",
"registry": true,
"raw": "object-scan@^13.0.0",
"name": "object-scan",
"escapedName": "object-scan",
"rawSpec": "^13.0.0",
"saveSpec": null,
"fetchSpec": "^13.0.0"
},
"_requiredBy": [
"/hafas-client"
],
"_resolved": "https://registry.npmjs.org/object-scan/-/object-scan-13.9.3.tgz",
"_shasum": "15778444a4d2a67bdd971b027816b179ea26bf3e",
"_spec": "object-scan@^13.0.0",
"_where": "/home/pi/MagicMirror/modules/MMM-PublicTransportHafas/node_modules/hafas-client",
"author": {
"name": "Lukas Siemon"
},
"bugs": {
"url": "https://github.com/blackflux/object-scan/issues"
},
"bundleDependencies": false,
"dependencies": {},
"deprecated": false,
"description": "Traverse object hierarchies using matching and callbacks.",
"devDependencies": {
"@babel/cli": "7.13.0",
"@babel/core": "7.13.8",
"@babel/register": "7.13.8",
"@blackflux/eslint-plugin-rules": "1.3.45",
"@blackflux/robo-config-plugin": "4.1.3",
"babel-eslint": "10.1.0",
"babel-preset-latest-node": "5.4.0",
"chai": "4.3.3",
"coveralls": "3.1.0",
"diff": "5.0.0",
"diff2html": "3.3.1",
"eslint": "7.21.0",
"eslint-config-airbnb-base": "14.2.1",
"eslint-plugin-import": "2.22.1",
"eslint-plugin-json": "2.1.2",
"eslint-plugin-markdown": "2.0.0",
"eslint-plugin-mocha": "8.1.0",
"joi-strict": "1.2.9",
"js-gardener": "2.0.187",
"json-stringify-pretty-compact": "3.0.0",
"lodash.isequal": "4.5.0",
"mustache": "4.1.0",
"node-tdd": "2.19.1",
"nyc": "15.1.0",
"object-scan": "13.9.1",
"semantic-release": "17.4.1",
"smart-fs": "1.12.5",
"stringify-object": "3.3.0",
"uuid": "8.3.2"
},
"engines": {
"node": ">= 10"
},
"files": [
"lib"
],
"homepage": "https://github.com/blackflux/object-scan#readme",
"keywords": [
"object",
"tree",
"traversal",
"iterate",
"recursive",
"glob",
"matching",
"regex",
"search",
"scan",
"find",
"walk",
"path",
"filter"
],
"license": "MIT",
"licenses": [
{
"type": "MIT",
"url": "https://github.com/blackflux/object-scan/blob/master/LICENSE"
}
],
"main": "lib/index.js",
"name": "object-scan",
"nyc": {
"tempDir": "./coverage/.nyc_output",
"report-dir": "./coverage",
"check-coverage": true,
"per-file": false,
"lines": 100,
"statements": 100,
"functions": 100,
"branches": 100,
"include": [
"**/*.js"
],
"reporter": [
"lcov",
"text-summary"
],
"require": [
"@babel/register"
],
"extension": [],
"cache": true,
"all": true,
"babel": true,
"exclude": [
"gardener.js",
"node_modules/*",
"coverage/*",
"lib/*",
"test/regression-test.js",
"test/worker.js"
]
},
"peerDependencies": {},
"repository": {
"type": "git",
"url": "git+https://github.com/blackflux/object-scan.git"
},
"scripts": {
"build": "npx babel src --out-dir lib --copy-files --include-dotfiles --config-file ./.babelrc",
"build-clean": "yarn run clean && yarn run build",
"clean": "rm -rf lib",
"coveralls": "node ./node_modules/coveralls/bin/coveralls.js < ./coverage/lcov.info",
"docker": "docker run --net host -u`id -u`:`id -g` -v $(pwd):/user/project -v ~/.aws:/user/.aws -v ~/.npmrc:/user/.npmrc -w /user/project -it --entrypoint /bin/bash",
"gardener": "node gardener",
"i": "yarn install --frozen-lockfile",
"it": "yarn run i && yarn run t",
"semantic-release": "yarn run build-clean && npx semantic-release",
"t": "yarn test",
"test": "yarn run clean && yarn run gardener && yarn run test-simple",
"test-simple": "nyc mocha \"./test/**/*.spec.js\"",
"ts": "yarn run test-simple",
"tsv": "yarn run test-simple --verbose",
"u": "yarn upgrade --latest --force"
},
"version": "13.9.3"
}