UNPKG

@thi.ng/zipper

Version:

Functional tree editing, manipulation & navigation

278 lines (277 loc) 5.99 kB
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 };