thi-ng/umbrella

View on GitHub
packages/zipper/src/zipper.ts

Summary

Maintainability
A
0 mins
Test Coverage
import type { FnO, Maybe } from "@thi.ng/api";
import { peek } from "@thi.ng/arrays/peek";
import { isArray } from "@thi.ng/checks/is-array";
import { assert } from "@thi.ng/errors/assert";
import type { Path, ZipperOps } from "./api.js";

/** @internal */
const __newPath = <T>(
    l: Maybe<T[]>,
    r: Maybe<T[]>,
    path: Maybe<Path<T>>,
    nodes: T[],
    changed = false
): Path<T> => ({
    l,
    r,
    path,
    nodes,
    changed,
});

/** @internal */
const __changedPath = <T>(path?: Path<T>) =>
    path ? { ...path, changed: true } : undefined;

export class Location<T> {
    protected readonly _node: T;
    protected readonly _ops: ZipperOps<T>;
    protected readonly _path: Maybe<Path<T>>;

    constructor(node: T, ops: ZipperOps<T>, path?: Path<T>) {
        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 : undefined;
    }

    get lefts() {
        return this._path ? this._path.l : undefined;
    }

    get rights() {
        return this._path ? this._path.r : undefined;
    }

    get left(): Maybe<Location<T>> {
        const path = this._path;
        const lefts = path && path.l;
        return lefts && 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
                    )
              )
            : undefined;
    }

    get right(): Maybe<Location<T>> {
        const path = this._path;
        const rights = path && 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 : undefined,
                path!.path,
                path!.nodes,
                path!.changed
            )
        );
    }

    get leftmost(): Location<T> {
        const path = this._path;
        const lefts = path && path.l;
        return lefts && lefts.length
            ? new Location(
                    lefts[0],
                    this._ops,
                    __newPath(
                        undefined,
                        lefts.slice(1).concat([this._node], path!.r || []),
                        path!.path,
                        path!.nodes,
                        path!.changed
                    )
              )
            : this;
    }

    get rightmost(): Location<T> {
        const path = this._path;
        const rights = path && path.r;
        return rights
            ? new Location(
                    peek(rights),
                    this._ops,
                    __newPath(
                        (path!.l || []).concat(
                            [this._node],
                            rights.slice(0, rights.length - 1)
                        ),
                        undefined,
                        path!.path,
                        path!.nodes,
                        path!.changed
                    )
              )
            : this;
    }

    get down(): Maybe<Location<T>> {
        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(
                undefined,
                r.length ? r : undefined,
                path,
                path ? path.nodes.concat([this._node]) : [this._node]
            )
        );
    }

    get up(): Maybe<Location<T>> {
        let path = this._path;
        const pnodes = path && 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(): T {
        const parent = this.up;
        return parent ? parent.root : this._node;
    }

    get prev(): Maybe<Location<T>> {
        let node = this.left;
        if (!node) return this.up;
        while (true) {
            const child: Maybe<Location<T>> = node!.isBranch
                ? node.down
                : undefined;
            if (!child) return node;
            node = child.rightmost;
        }
    }

    get next(): Maybe<Location<T>> {
        if (this.isBranch) return this.down;
        let right = this.right;
        if (right) return right;
        // eslint-disable-next-line no-this-alias -- zipper traversal
        let loc: Location<T> = this;
        while (true) {
            const up = loc.up;
            if (!up) return;
            right = up.right;
            if (right) return right;
            loc = up;
        }
    }

    replace(x: T) {
        return new Location(x, this._ops, __changedPath(this._path));
    }

    update(fn: FnO<T, T>, ...args: any[]) {
        return this.replace(fn(this._node, ...args));
    }

    insertLeft(x: T) {
        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: T) {
        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: T) {
        this.ensureBranch();
        return this.replace(this.newNode(this._node, [x, ...this.children]));
    }

    appendChild(x: T) {
        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 : undefined;
                if (!child) return loc;
                loc = child.rightmost;
            }
        }
        return new Location(
            this.newNode(peek(path.nodes), path.r || []),
            this._ops,
            __changedPath(path.path)
        );
    }

    protected newNode(node: T, children: T[]) {
        return this._ops.factory(node, children);
    }

    protected ensureNotRoot() {
        assert(!!this._path, "can't insert at root level");
    }

    private ensureBranch() {
        assert(this.isBranch, "can only insert in branches");
    }
}

export const zipper = <T>(ops: ZipperOps<T>, node: T): Location<T> =>
    new Location<T>(node, ops);

export const arrayZipper = <T>(root: T[]) =>
    zipper<T | T[]>(
        {
            branch: isArray,
            children: (x) => <T[]>x,
            factory: (_, xs) => <T[]>xs,
        },
        root
    );