Installation
Quick Take
The challenge
Operations on AST’s — Abstract Syntax Trees — or anything deeply nested are difficult.
The main challenge is going “up the branch” — querying the parent and sibling nodes.
Second challenge, AST’s get VERY BIG very quickly. A single tag, <td>a</td>
, 10 characters produced 398 characters of AST above. Enormous inputs are very hard to reason about, especially to troubleshoot, printed trees don’t fit into screen.
The “Going up” is often solved by putting circular references in the parsed tree, like "parent": "[Circular ~.0]",
. The first drawback of using circular references is that it’s not standard JSON, you can’t even JSON.stringify
(specialised stringification packages do exist) — everything from the algorithm up to the unit test runner are affected. The second drawback of circular references is that while they make it easier to query things, they also make it harder to amend things — you have to amend “circular extras” as well (or hope renderer will be OK, but that’s only for small operations).
This program doesn’t rely on circular references. It uses indexing of “breadcrumb” paths. For example, you traverse and find that node you want is index 58
, whole path being [2, 14, 16, 58]
. You save the path down. After the traversal is done, you fetch the monkey to delete the index 58
. You can also use a for
loop on breadcrumb index array, [2, 14, 16, 58]
and fetch and check parent 16
and grandparent 14
. Lots of possibilities. Function find() searches using key or value or both, and function get() searches using a known index. That’s the strategy.
Idea
Conceptually, we use two systems to mark paths in AST:
- Our unique, number-based indexing system — each encountered node is numbered, for example,
58
(along with “breadcrumb” path, an array of integers, for example,[2, 14, 16, 58]
). If you know the number you can get monkey to fetch you the node at that number or make amends on it. object-path
notation, as infoo.1.bar
(instead offoo[1].bar
). The dot marking system is also powerful, it is used in many our programs, althouh it has some shortcomings (no dots in key names, for example).
Traversal function will report both ways.
API — find()
This function can search inside JSON-like data structures for plain objects by key or by value or by both and return the indexes path to an each finding.
The function find()
is imported like this:
It takes two input arguments:
Input argument | Type | Obligatory | Description |
---|---|---|---|
input Type: Whatever Obligatory: yes | |||
input | Whatever | yes | JSON-like data structure. |
options Type: Plain object Obligatory: yes | |||
options | Plain object | yes | Obligatory Options Object. |
The Obligatory Options Object has the following shape:
Key | Type | Obligatory | Description |
---|---|---|---|
key Type: String Obligatory: at least one, key or val | |||
key | String | at least one, key or val | If you want to search by a plain object’s key, put it here. |
val Type: Whatever Obligatory: at least one, key or val | |||
val | Whatever | at least one, key or val | If you want to search by a plain object’s value, put it here. |
only Type: undefined or null or "any" or "array" or "object" Obligatory: no (defaults to any ) | |||
only | undefined or null or "any" or "array" or "object" | no (defaults to any ) | You can specify, to search only within arrays, objects or anything. |
Either opts.key
or opts.val
or both must be present. If both are missing, find()
will throw an error.
Output
The output will be an array, comprising of zero or more plain objects in the following format:
A finding object’s key | Type | Description |
---|---|---|
index Type: Integer number | ||
index | Integer number | The index of the finding. It’s also the last element of the path array. |
key Type: String | ||
key | String | The found object’s key |
val Type: Whatever or null | ||
val | Whatever or null | The found object’s value (or null if it’s a key of an array) |
path Type: Array | ||
path | Array | The found object’s path: indexes of all its parents, starting from the topmost. The found key/value pair’s address will be the last element of the path array. |
If a finding is an element of an array, the val
will be set to null
.
A use example
Find out, what is the path to the key that equals ‘b’.
import {
find,
get,
set,
drop,
del,
arrayFirstOnly,
traverse,
} from "ast-monkey";
const input = ["a", [["b"], "c"]];
const key = "b";
const result = find(input, { key: key });
console.log("result = " + JSON.stringify(result, null, 4));
// => [
// {
// index: 4,
// key: 'b',
// val: null,
// path: [2, 3, 4]
// }
// ]
Once you know that the path is [2, 3, 4]
, you can iterate its parents, get()
-ing indexes number 3
and 2
and perform operations on it. The last element in the findings array is the finding itself.
This method is the most versatile of the ast-monkey
because you can go “up the AST tree” by querying its array elements backwards.
API — get()
Use method get()
to query AST trees by branch’s index (a numeric id). You would get that index from a previously performed find()
or you can pick a number manually.
The get()
is used on each element of the findings array (which you would get after performing find()
). Then, depending on your needs, you would write the particular index over using set()
or delete it using drop()
.
The function get()
is imported like this:
It’s a function which takes two input arguments:
Input argument | Type | Obligatory | Description |
---|---|---|---|
input Type: Whatever Obligatory: yes | |||
input | Whatever | yes | JSON-like data structure. |
opts Type: Plain object Obligatory: yes | |||
opts | Plain object | yes | Obligatory Options Object. |
The Obligatory Options Object has the following shape:
Key | Type | Obligatory | Description |
---|---|---|---|
index Type: Number or number-as-string Obligatory: yes | |||
index | Number or number-as-string | yes | Index number of piece of AST you want the monkey to retrieve for you. |
only Type: undefined or null or "any" or "array" or "object" Obligatory: no (defaults to any ) | |||
only | undefined or null or "any" or "array" or "object" | no (defaults to any ) | You can specify, to search only within arrays, objects or anything. |
Output
The get()
returns object, array or null
, depending what index was matched (or not).
A use example
If you know that you want an index number two, you can query it using get()
:
import {
find,
get,
set,
drop,
del,
arrayFirstOnly,
traverse,
} from "ast-monkey";
const input = {
a: {
b: "c",
},
};
const index = 2;
const result = get(input, { index: index });
console.log("result = " + JSON.stringify(result, null, 4));
// => {
// b: 'c'
// }
In practice, you would query a list of indexes programmatically using a for
loop.
API — set()
Use set()
to overwrite a piece of an AST when you know its index.
The function set()
is imported like this:
It’s a function which takes two input arguments:
Input argument | Type | Obligatory | Description |
---|---|---|---|
input Type: Whatever Obligatory: yes | |||
input | Whatever | yes | JSON-like data structure. |
options Type: Object Obligatory: yes | |||
options | Object | yes | Obligatory Options Object. |
The Obligatory Options Object has the following shape:
Key | Type | Obligatory | Description |
---|---|---|---|
index Type: Number or number-as-string Obligatory: yes | |||
index | Number or number-as-string | yes | Index of the piece of AST to find and replace |
val Type: Whatever Obligatory: yes | |||
val | Whatever | yes | Value to replace the found piece of AST with |
Output
Function returns an amended cloned input.
A use example
Let’s say you identified the index
of a piece of AST you want to write over:
import {
find,
get,
set,
drop,
del,
arrayFirstOnly,
traverse,
} from "ast-monkey";
const input = {
a: { b: [{ c: { d: "e" } }] },
f: { g: ["h"] },
};
const index = "7";
const val = "zzz";
const result = set(input, { index: index, val: val });
console.log("result = " + JSON.stringify(result, null, 4));
// => {
// a: {b: [{c: {d: 'e'}}]},
// f: {g: 'zzz'}
// }
API — drop()
Use drop()
to delete a piece of an AST with a known index.
The function drop()
is imported like this:
It takes two input arguments:
Input argument | Type | Obligatory | Description |
---|---|---|---|
input Type: Whatever Obligatory: yes | |||
input | Whatever | yes | JSON-like data structure. |
options Type: Object Obligatory: yes | |||
options | Object | yes | Obligatory Options Object. |
The Obligatory Options Object has the following shape:
Key | Type | Obligatory | Description |
---|---|---|---|
index Type: Number or number-as-string Obligatory: yes | |||
index | Number or number-as-string | yes | Index number of piece of AST you want the monkey to delete for you. |
Output
Function returns an amended cloned input.
A use example
Let’s say you want to delete the piece of AST with an index number 8. That’s 'h'
:
import {
find,
get,
set,
drop,
del,
arrayFirstOnly,
traverse,
} from "ast-monkey";
const input = {
a: { b: [{ c: { d: "e" } }] },
f: { g: ["h"] },
};
const index = "8"; // can be integer as well
const result = drop(input, { index: index });
console.log("result = " + JSON.stringify(result, null, 4));
// => {
// a: {b: [{c: {d: 'e'}}]},
// f: {g: []}
// }
API — del()
Use del()
to delete all chosen key/value pairs from all objects found within an AST, or all chosen elements from all arrays.
The function del()
is imported like this:
It takes two input arguments:
Input argument | Type | Obligatory | Description |
---|---|---|---|
input Type: Whatever Obligatory: yes | |||
input | Whatever | yes | JSON-like data structure. |
opts Type: Plain object Obligatory: yes | |||
opts | Plain object | yes | Obligatory Options Object. See below. |
The Obligatory Options Object has the following shape:
Key | Type | Obligatory | Description |
---|---|---|---|
key Type: String Obligatory: at least one, key or val | |||
key | String | at least one, key or val | All keys in objects or elements in arrays will be selected for deletion |
val Type: Whatever Obligatory: at least one, key or val | |||
val | Whatever | at least one, key or val | All object key/value pairs having this value will be selected for deletion |
only Type: String Obligatory: no (if not given, will default to any ) | |||
only | String | no (if not given, will default to any ) | You can specify, to delete key/value pairs of an object or only elements on an array. |
If you set only key
, any value will be deleted as long as key
matches. Same with specifying only val
. If you specify both, both will have to match; otherwise, key/value pair (in objects) will not be deleted. Since arrays won’t have any val
ues, no elements in arrays will be deleted if you set both key
and val
.
Output
Function returns an amended cloned input.
A use example
Let’s say you want to delete all key/value pairs from objects that have a key equal to ‘c’. Value does not matter.
import {
find,
get,
set,
drop,
del,
arrayFirstOnly,
traverse,
} from "ast-monkey";
const input = {
a: { b: [{ c: { d: "e" } }] },
c: { d: ["h"] },
};
const key = "c";
const result = del(input, { key: key });
console.log("result = " + JSON.stringify(result, null, 4));
// => {
// a: {b: [{}]}
// }
API — arrayFirstOnly()
arrayFirstOnly()
will take an input (whatever), if it’s traversable, it will traverse it, leaving only the first element within each array it encounters.
The function arrayFirstOnly()
is imported like this:
It takes one input argument:
Input argument | Type | Obligatory | Description |
---|---|---|---|
input Type: Whatever Obligatory: yes | |||
input | Whatever | yes | JSON-like data structure. |
Output
Function returns an amended cloned input.
A use example
import {
find,
get,
set,
drop,
del,
arrayFirstOnly,
traverse,
} from "ast-monkey";
const input = [
{
a: "a",
},
{
b: "b",
},
];
const result = arrayFirstOnly(input);
console.log("result = " + JSON.stringify(result, null, 4));
// => [
// {
// a: 'a'
// }
// ]
In practice, it’s handy when you want to simplify the data objects. For example, all our email templates have content separated from the template layout. Content sits in index.json
file. For dev purposes, we want to show, let’s say two products in the shopping basket listing. However, in a production build, we want to have only one item, but have it sprinkled with back-end code (loop logic and so on). This means, we have to take data object meant for a dev build, and flatten all arrays in the data, so they contain only the first element. ast-monkey
comes to help.
API — traverse()
The function traverse()
is imported like this:
traverse()
is re-exported from an npm package ast-monkey-traverse. For detailed API description, see its documentation.