@thi.ng/zipper
Version:
Functional tree editing, manipulation & navigation
278 lines (277 loc) • 5.99 kB
JavaScript
import { peek } from "@thi.ng/arrays/peek";
import { isArray } from "@thi.ng/checks/is-array";
import { assert } from "@thi.ng/errors/assert";
const __newPath = (l, r, path, nodes, changed = false) => ({
l,
r,
path,
nodes,
changed
});
const __changedPath = (path) => path ? { ...path, changed: true } : void 0;
class Location {
_node;
_ops;
_path;
constructor(node, ops, path) {
this._node = node;
this._ops = ops;
this._path = path;
}
get isBranch() {
return this._ops.branch(this._node);
}
get isFirst() {
return !this.lefts;
}
get isLast() {
return !this.rights;
}
get depth() {
let d = 0;
let path = this._path;
while (path) {
d++;
path = path.path;
}
return d;
}
get node() {
return this._node;
}
get children() {
return this._ops.children(this._node);
}
get path() {
return this._path ? this._path.nodes : void 0;
}
get lefts() {
return this._path ? this._path.l : void 0;
}
get rights() {
return this._path ? this._path.r : void 0;
}
get left() {
const path = this._path;
const lefts = path?.l;
return lefts?.length ? new Location(
peek(lefts),
this._ops,
__newPath(
lefts.slice(0, lefts.length - 1),
[this._node].concat(path.r || []),
path.path,
path.nodes,
path.changed
)
) : void 0;
}
get right() {
const path = this._path;
const rights = path?.r;
if (!rights) return;
const r = rights.slice(1);
return new Location(
rights[0],
this._ops,
__newPath(
(path.l || []).concat([this._node]),
r.length ? r : void 0,
path.path,
path.nodes,
path.changed
)
);
}
get leftmost() {
const path = this._path;
const lefts = path?.l;
return lefts?.length ? new Location(
lefts[0],
this._ops,
__newPath(
void 0,
lefts.slice(1).concat([this._node], path.r || []),
path.path,
path.nodes,
path.changed
)
) : this;
}
get rightmost() {
const path = this._path;
const rights = path?.r;
return rights ? new Location(
peek(rights),
this._ops,
__newPath(
(path.l || []).concat(
[this._node],
rights.slice(0, rights.length - 1)
),
void 0,
path.path,
path.nodes,
path.changed
)
) : this;
}
get down() {
if (!this.isBranch) return;
const children = this.children;
if (!children) return;
const path = this._path;
const r = children.slice(1);
return new Location(
children[0],
this._ops,
__newPath(
void 0,
r.length ? r : void 0,
path,
path ? path.nodes.concat([this._node]) : [this._node]
)
);
}
get up() {
let path = this._path;
const pnodes = path?.nodes;
if (!pnodes) return;
const pnode = peek(pnodes);
if (path.changed) {
return new Location(
this.newNode(
pnode,
(path.l || []).concat([this._node], path.r || [])
),
this._ops,
__changedPath(path.path)
);
} else {
return new Location(pnode, this._ops, path.path);
}
}
get root() {
const parent = this.up;
return parent ? parent.root : this._node;
}
get prev() {
let node = this.left;
if (!node) return this.up;
while (true) {
const child = node.isBranch ? node.down : void 0;
if (!child) return node;
node = child.rightmost;
}
}
get next() {
if (this.isBranch) return this.down;
let right = this.right;
if (right) return right;
let loc = this;
while (true) {
const up = loc.up;
if (!up) return;
right = up.right;
if (right) return right;
loc = up;
}
}
replace(x) {
return new Location(x, this._ops, __changedPath(this._path));
}
update(fn, ...args) {
return this.replace(fn(this._node, ...args));
}
insertLeft(x) {
this.ensureNotRoot();
const path = this._path;
return new Location(
this._node,
this._ops,
__newPath(
path.l ? path.l.concat([x]) : [x],
path.r,
path.path,
path.nodes,
true
)
);
}
insertRight(x) {
this.ensureNotRoot();
const path = this._path;
return new Location(
this._node,
this._ops,
__newPath(
path.l,
[x].concat(path.r || []),
path.path,
path.nodes,
true
)
);
}
insertChild(x) {
this.ensureBranch();
return this.replace(this.newNode(this._node, [x, ...this.children]));
}
appendChild(x) {
this.ensureBranch();
return this.replace(
this.newNode(this._node, this.children.concat([x]))
);
}
remove() {
this.ensureNotRoot();
const path = this._path;
const lefts = path.l;
if (lefts ? lefts.length : 0) {
let loc = new Location(
peek(lefts),
this._ops,
__newPath(
lefts.slice(0, lefts.length - 1),
path.r,
path.path,
path.nodes,
true
)
);
while (true) {
const child = loc.isBranch ? loc.down : void 0;
if (!child) return loc;
loc = child.rightmost;
}
}
return new Location(
this.newNode(peek(path.nodes), path.r || []),
this._ops,
__changedPath(path.path)
);
}
newNode(node, children) {
return this._ops.factory(node, children);
}
ensureNotRoot() {
assert(!!this._path, "can't insert at root level");
}
ensureBranch() {
assert(this.isBranch, "can only insert in branches");
}
}
const zipper = (ops, node) => new Location(node, ops);
const arrayZipper = (root) => zipper(
{
branch: isArray,
children: (x) => x,
factory: (_, xs) => xs
},
root
);
export {
Location,
arrayZipper,
zipper
};