zippa
Version:
A Generic Zipper Library
967 lines (842 loc) • 22.8 kB
JavaScript
import __ from 'ramda/src/__';
import assoc from 'ramda/src/assoc';
import arrOf from 'ramda/src/of';
import always from 'ramda/src/always';
import call from 'ramda/src/call';
import cond from 'ramda/src/cond';
import converge from 'ramda/src/converge';
import complement from 'ramda/src/complement';
import curry from 'ramda/src/curry';
import either from 'ramda/src/either';
import head from 'ramda/src/head';
import equals from 'ramda/src/equals';
import identity from 'ramda/src/identity';
import init from 'ramda/src/init';
import ifElse from 'ramda/src/ifElse';
import last from 'ramda/src/last';
import or from 'ramda/src/or';
import prop from 'ramda/src/prop';
import juxt from 'ramda/src/juxt';
import tail from 'ramda/src/tail';
import pipe from 'ramda/src/pipe';
import merge from 'ramda/src/merge';
import alwaysTrue from 'ramda/src/T';
import when from 'ramda/src/when';
import unless from 'ramda/src/unless';
import until from 'ramda/src/until';
import unnest from 'ramda/src/unnest';
const TOP = null;
const END = 'END';
function isEmpty(arr) {
return !arr.length;
}
export const whilst = curry((predicate, fn, value) => {
let curr = value;
while (predicate(curr)) {
curr = fn(curr);
}
return curr;
});
const TOPPATH = {
left: [],
right: [],
parentItems: TOP,
parentPath: TOP,
changed: false,
};
/**
* The Zipper class.
*
* Keeps track of the current item, path, and metadata (implementation functions).
*
* Don't use this constructor directly. Create your own Zipper factory with `makeZipper`,
* and use it to create instances of Zipper.
*
* @class Zipper
* @namespace Zipper
*/
function Zipper(item, path, meta) {
this.item = item;
this.path = path;
this.meta = meta;
}
const getItem = prop('item');
/**
* Gets the value of the current location.
* @param {Zipper} zipper
* @returns {T|null}
*/
export const value = getItem;
const getPath = prop('path');
const getMeta = prop('meta');
const sideEffect = curry((fn, x) => {
fn(x);
return x;
});
const _raiser = msg => () => {
throw new Error(msg);
};
const raise = pipe(_raiser, sideEffect);
export function zipperFrom(oldLoc, newItem, path, meta) {
return new Zipper(newItem, path || getPath(oldLoc), meta || getMeta(oldLoc));
}
/**
* Returns a boolean indicating if the current location is not a leaf.
* @param {Zipper} zipper
* @returns {boolean}
*/
export function isBranch(zipper) {
return getMeta(zipper).isBranch(getItem(zipper));
}
/**
* Returns a boolean indicating if the current location is a leaf.
* @param {Zipper} zipper
* @returns {boolean}
*/
export const isLeaf = complement(isBranch);
const _getChildrenFn = pipe(getMeta, prop('getChildren'));
export const getChildren = pipe(
when(isLeaf, raise('Tried getting children of a leaf')),
converge(call, [_getChildrenFn, getItem]),
);
function makeItem(z, item, children) {
return getMeta(z).makeItem(item, children);
}
/**
* Returns a boolean indicating if the zipper has been
* exhausted by calls to `next`.
*
* @param {Zipper} zipper
* @returns {boolean}
*/
export const isEnd = pipe(getPath, equals(END));
const _parentItemsFromPath = prop('parentItems');
const _parent = pipe(_parentItemsFromPath, last);
const _parentPath = prop('parentPath');
const getParentItems = pipe(
getPath,
when(equals(END), raise('Can\'t get parent items from end path.')),
_parentItemsFromPath
);
const getParent = pipe(getPath, _parent);
const getParentPath = pipe(getPath, _parentPath);
/**
* Returns a boolean indicating if the zipper is at the top.
* @param {Zipper} zipper
* @returns {boolean}
*/
export const isTop = pipe(getParentItems, equals(TOP));
/**
* Returns a boolean indicating if the zipper is not at the top.
* @param {Zipper} zipper
* @returns {boolean}
*/
export const isNotTop = complement(isTop);
const _leftsFromPath = pipe(prop('left'), or(__, []));
const _rightsFromPath = pipe(prop('right'), or(__, []));
const lefts = pipe(getPath, _leftsFromPath);
const rights = pipe(getPath, _rightsFromPath);
const hasChanged = pipe(getPath, prop('changed'), Boolean);
const isUnchanged = complement(hasChanged);
const isNotEmpty = complement(isEmpty);
/**
* Returns a boolean indicating if the item at the current location
* is the leftmost sibling.
* @param {Zipper} zipper
* @returns {boolean}
*/
export const isLeftmost = pipe(lefts, isEmpty);
/**
* Returns a boolean indicating if the item at the current location
* is the rightmost sibling.
* @param {Zipper} zipper
* @returns {boolean}
*/
export const isRightmost = pipe(rights, isEmpty);
/**
* Returns a boolean indicating if the item at the current location
* is the leftmost sibling.
*
* Alias for {@link isLeftmost}
* @param {Zipper} zipper
* @returns {boolean}
*/
export const canGoLeft = complement(isLeftmost);
/**
* Returns a boolean indicating if the item at the current location
* is the rightmost sibling.
*
* Alias for {@link isRightmost}
* @param {Zipper} zipper
* @returns {boolean}
*/
export const canGoRight = complement(isRightmost);
/**
* Moves location to the leftmost sibling.
* If the current location is already the leftmost,
* returns itself.
*
* @param {Zipper} zipper
* @returns {Zipper}
*/
export function leftmost(zipper) {
if (isTop(zipper) || isLeftmost(zipper)) return zipper;
const path = getPath(zipper);
const _lefts = _leftsFromPath(path);
const _rights = _rightsFromPath(path);
const item = getItem(zipper);
const leftMost = head(_lefts);
const newLeft = [];
const newRight = tail(_lefts).concat([item], _rights);
return zipperFrom(
zipper,
leftMost,
merge(path, {
left: newLeft,
right: newRight,
}),
);
}
/**
* Moves location to the left sibling.
* If the current location is already the leftmost,
* returns null.
*
* @param {Zipper} zipper
* @returns {Zipper|null}
*/
export function left(zipper) {
if (isEnd(zipper)) return zipper;
if (isLeftmost(zipper)) return null;
const item = getItem(zipper);
const path = getPath(zipper);
const _lefts = _leftsFromPath(path);
const _rights = _rightsFromPath(path);
const leftSibling = last(_lefts);
const newLeft = init(_lefts);
const newRight = [item].concat(_rights);
return zipperFrom(
zipper,
leftSibling,
merge(path, {
left: newLeft,
right: newRight,
}),
);
}
/**
* Moves location to the right sibling.
* If the current location is already the rightmost,
* returns null.
*
* @param {Zipper} zipper
* @returns {Zipper|null}
*/
export function right(zipper) {
if (isEnd(zipper)) return zipper;
if (isRightmost(zipper)) return null;
const item = getItem(zipper);
const path = getPath(zipper);
const _lefts = _leftsFromPath(path);
const _rights = _rightsFromPath(path);
const rightSibling = head(_rights);
const newLeft = _lefts.concat([item]);
const newRight = tail(_rights);
return zipperFrom(
zipper,
rightSibling,
merge(path, {
left: newLeft,
right: newRight,
}),
);
}
/**
* Moves location to the rightmost sibling.
* If the current location is already the rightmost,
* returns itself.
*
* @param {Zipper} zipper
* @returns {Zipper}
*/
export function rightmost(zipper) {
if (isRightmost(zipper)) return zipper;
const path = getPath(zipper);
const _rights = _rightsFromPath(path);
const _lefts = _leftsFromPath(path);
const item = getItem(zipper);
const rightMost = last(_rights);
const newLeft = _lefts.concat([item], init(_rights));
const newRight = [];
return zipperFrom(
zipper,
rightMost,
merge(path, {
left: newLeft,
right: newRight,
})
);
}
/**
* Alias for `isBranch`
*
* @param {Zipper} zipper
* @returns {boolean}
*/
export function canGoDown(ipper) {
return isBranch(ipper) && isNotEmpty(getChildren(ipper));
}
/**
* Moves location to the leftmost child.
* If the current item is a leaf, returns null.
*
* @param {Zipper} zipper
* @returns {Zipper|null}
*/
export function down(zipper) {
if (!isBranch(zipper)) return null;
const item = getItem(zipper);
const path = getPath(zipper);
const children = getChildren(zipper);
const newLeft = [];
const newRight = tail(children);
const newLoc = zipperFrom(
zipper,
head(children),
merge(path, {
left: newLeft,
right: newRight,
parentItems: (_parentItemsFromPath(path) || []).concat([item]),
parentPath: path,
})
);
return newLoc;
}
function _insertLeft(insertItem, zipper) {
if (isTop(zipper)) {
throw new Error('Tried inserting left of top');
}
const item = getItem(zipper);
const path = getPath(zipper);
const _lefts = _leftsFromPath(path);
return zipperFrom(
zipper,
item,
merge(path, {
left: _lefts.concat([insertItem]),
changed: true,
})
);
}
function _insertRight(insertItem, zipper) {
if (isTop(zipper)) {
throw new Error('Tried inserting left of top');
}
const item = getItem(zipper);
const path = getPath(zipper);
const _rights = _rightsFromPath(path);
return zipperFrom(
zipper,
item,
merge(path, {
right: [insertItem].concat(_rights),
changed: true,
})
);
}
function _replace(_replaceWith, zipper) {
if (getItem(zipper) === _replaceWith) return zipper;
return zipperFrom(
zipper,
_replaceWith,
assoc('changed', true, getPath(zipper))
);
}
function _edit(fn, zipper) {
return _replace(fn(getItem(zipper)), zipper);
}
/**
* Returns a boolean indicating if the zipper is not at the top.
*
* Alias for {@link isNotTop}
*
* @param {Zipper} zipper
* @returns {boolean}
*/
export const canGoUp = isNotTop;
const _unchangedUp = converge(zipperFrom, [identity, getParent, getParentPath]);
const itemsOnCurrentLevel = pipe(
juxt([lefts, pipe(getItem, arrOf), rights]),
unnest
);
const makeParentItem = converge(makeItem, [identity, getParent, itemsOnCurrentLevel]);
/**
* Moves location to the parent, constructing a new parent
* if the children have changed.
*
* If already at the top, returns null.
*
* @param {Zipper} zipper
* @returns {Zipper|null}
*/
export const up = cond([
[or(isTop, isEnd), always(null)],
[isUnchanged, _unchangedUp],
[alwaysTrue, converge(
zipperFrom,
[
identity,
makeParentItem,
pipe(getParentPath, assoc('changed', true)),
]
)],
]);
/**
* Moves location to the root, constructing
* any changes made.
*
* @param {Zipper} zipper
* @returns {Zipper}
*/
export const root = unless(isEnd, whilst(isNotTop, up));
const toEnd = z => zipperFrom(z, getItem(z), END);
const nextUp = pipe(
until(either(isTop, canGoRight), up),
ifElse(
isTop,
toEnd,
right,
)
);
/**
* Moves location to the next element in depth-first order.
*
* @param {Zipper} zipper
* @returns {Zipper}
*/
export const next = cond([
[isEnd, identity],
[canGoDown, down],
[canGoRight, right],
[alwaysTrue, nextUp],
]);
const _goToRightmostChild = pipe(down, rightmost);
/**
* Moves location to the previous element in depth-first order.
*
* @param {Zipper} zipper
* @returns {Zipper}
*/
export const prev = ifElse(
canGoLeft,
pipe(left, whilst(isBranch, _goToRightmostChild)),
up,
);
/**
* Removes item at the current location.
* Returns location that would be previous in depth first search.
*
* @param {Zipper} zipper
* @returns {Zipper}
*/
export function remove(zipper) {
if (isTop(zipper)) {
throw new Error('Can\'t remove top.');
}
if (isEnd(zipper)) throw new Error('Can\'t remove end');
const path = getPath(zipper);
const _lefts = _leftsFromPath(path);
if (_lefts.length) {
const leftSibling = zipperFrom(
zipper,
last(_lefts),
merge(path, {
left: init(_lefts),
changed: true,
})
);
return whilst(isBranch, _goToRightmostChild, leftSibling);
} else {
const _rights = _rightsFromPath(path);
const parentPath = _parentPath(path);
const parent = _parent(path);
return zipperFrom(
zipper,
makeItem(zipper, parent, _rights),
isTop(zipper)
? parentPath
: assoc('changed', true, parentPath)
);
}
}
function _insertChild(item, z) {
const newChildren = [item].concat(getChildren(z));
return _replace(makeItem(z, item, newChildren), z);
}
function _appendChild(item, z) {
const newChildren = getChildren(z).concat([item]);
return _replace(makeItem(z, item, newChildren), z);
}
function makeNullaryMethod(fn) {
return function nullaryZipperMethod() {
return fn(this);
};
}
function makeUnaryMethod(fn) {
return function unaryZipperMethod(x) {
return fn(x, this);
};
}
Object.assign(Zipper.prototype, {
/**
* Gets the value of the current location.
*
* @alias Zipper.prototype.value
* @instance
* @memberof Zipper
* @returns {T|null}
*/
value: makeNullaryMethod(value),
/**
* Moves location to the root, constructing
* any changes made.
*
* @alias Zipper.prototype.root
* @instance
* @memberof Zipper
* @returns {Zipper}
*/
root: makeNullaryMethod(root),
/**
* Moves location to the parent.
* If at the top, returns null.
*
* @alias Zipper.prototype.up
* @instance
* @memberof Zipper
* @returns {Zipper|null}
*/
up: makeNullaryMethod(up),
/**
* Moves location to the leftmost child.
* If the current item is a leaf, returns null.
*
* @alias Zipper.prototype.down
* @instance
* @memberof Zipper
* @returns {Zipper|null}
*/
down: makeNullaryMethod(down),
/**
* Moves location to the left sibling.
* If the current location is already the leftmost,
* returns null.
*
* @alias Zipper.prototype.left
* @instance
* @memberof Zipper
* @returns {Zipper|null}
*/
left: makeNullaryMethod(left),
/**
* Moves location to the right sibling.
* If the current location is already the rightmost,
* returns null.
*
* @alias Zipper.prototype.right
* @instance
* @memberof Zipper
* @returns {Zipper|null}
*/
right: makeNullaryMethod(right),
/**
* Moves location to the leftmost sibling.
* If the current location is already the leftmost,
* returns itself.
*
* @alias Zipper.prototype.leftmost
* @instance
* @memberof Zipper
* @returns {Zipper}
*/
leftmost: makeNullaryMethod(leftmost),
/**
* Moves location to the rightmost sibling.
* If the current location is already the rightmost,
* returns itself.
*
* @alias Zipper.prototype.rightmost
* @instance
* @memberof Zipper
* @returns {Zipper}
*/
rightmost: makeNullaryMethod(rightmost),
/**
* Moves location to the next element in depth-first order.
*
* @alias Zipper.prototype.next
* @instance
* @memberof Zipper
* @returns {Zipper}
*/
next: makeNullaryMethod(next),
/**
* Moves location to the previous element in depth-first order.
*
* @alias Zipper.prototype.prev
* @instance
* @memberof Zipper
* @returns {Zipper}
*/
prev: makeNullaryMethod(prev),
/**
* Returns a boolean indicating if the zipper has been
* exhausted by calls to `next`.
*
* @alias Zipper.prototype.isEnd
* @instance
* @memberof Zipper
* @returns {boolean}
*/
isEnd: makeNullaryMethod(isEnd),
/**
* Returns a boolean indicating if the zipper is at the top.
*
* @alias Zipper.prototype.isTop
* @instance
* @memberof Zipper
* @returns {boolean}
*/
isTop: makeNullaryMethod(isTop),
/**
* Returns a boolean indicating if the current location is not a leaf.
*
* @alias Zipper.prototype.isBranch
* @instance
* @memberof Zipper
* @returns {boolean}
*/
isBranch: makeNullaryMethod(isBranch),
/**
* Returns a boolean indicating if the current location is a leaf.
*
* @alias Zipper.prototype.isLeaf
* @instance
* @memberof Zipper
* @returns {boolean}
*/
isLeaf: makeNullaryMethod(isLeaf),
/**
* Returns a boolean indicating if the item at the current location
* is the leftmost sibling.
*
* @alias Zipper.prototype.isLeftmost
* @instance
* @memberof Zipper
* @returns {boolean}
*/
isLeftmost: makeNullaryMethod(isLeftmost),
/**
* Returns a boolean indicating if the item at the current location
* is the rightmost sibling.
*
* @alias Zipper.prototype.isRightmost
* @instance
* @memberof Zipper
* @returns {boolean}
*/
isRightmost: makeNullaryMethod(isRightmost),
/**
* Alias for `isTop`.
*
* @alias Zipper.prototype.canGoUp
* @instance
* @memberof Zipper
* @returns {boolean}
*/
canGoUp: makeNullaryMethod(canGoUp),
/**
* Alias for `isLeftmost`
*
* @alias Zipper.prototype.canGoLeft
* @instance
* @memberof Zipper
* @returns {boolean}
*/
canGoLeft: makeNullaryMethod(canGoLeft),
/**
* Alias for `isRightmost`
*
* @alias Zipper.prototype.canGoRight
* @instance
* @memberof Zipper
* @returns {boolean}
*/
canGoRight: makeNullaryMethod(canGoRight),
/**
* Alias for `isBranch`
*
* @alias Zipper.prototype.canGoDown
* @instance
* @memberof Zipper
* @returns {boolean}
*/
canGoDown: makeNullaryMethod(canGoDown),
/**
* Replaces the current item with value returned
* by calling `fn` with the current item.
*
* @alias Zipper.prototype.edit
* @instance
* @memberof Zipper
* @param {Function} fn - Function that takes the old item
* and returns a new item.
* @returns {Zipper}
*/
edit: makeUnaryMethod(_edit),
/**
* Replaces the current item with the given value.
*
* @alias Zipper.prototype.replace
* @instance
* @memberof Zipper
* @param {T} replaceWith - item to replace the current one with.
* @returns {Zipper}
*/
replace: makeUnaryMethod(_replace),
/**
* Inserts a new item as the left sibling.
*
* @alias Zipper.prototype.insertLeft
* @instance
* @memberof Zipper
* @param {T} item
* @returns {Zipper}
*/
insertLeft: makeUnaryMethod(_insertLeft),
/**
* Inserts a new item as the right sibling.
*
* @alias Zipper.prototype.insertRight
* @instance
* @memberof Zipper
* @param {T} item
* @returns {Zipper}
*/
insertRight: makeUnaryMethod(_insertRight),
/**
* Inserts a new item as the leftmost child.
*
* @alias Zipper.prototype.insertChild
* @instance
* @memberof Zipper
* @param {T} item
* @returns {Zipper}
*/
insertChild: makeUnaryMethod(_insertChild),
/**
* Inserts a new item as the rightmost child.
*
* @alias Zipper.prototype.appendChild
* @instance
* @memberof Zipper
* @param {T} item
* @returns {Zipper}
*/
appendChild: makeUnaryMethod(_appendChild),
/**
* Removes item at the current location.
* Returns location that would be previous in depth first search.
*
* @alias Zipper.prototype.remove
* @instance
* @memberof Zipper
* @returns {Zipper}
*/
remove: makeNullaryMethod(remove),
});
/**
* Makes a Zipper factory that uses the implementation provided
* in the parameters.
*
* @param {Function} _isBranch - Function with signature`(item: T) => boolean`
* that indicates if the item can have children.
* @param {Function} _getChildren - Function with signature`(item: T) => Array<T>`
* that returns an array of children for a branch.
* @param {Function} _makeItem - Function with signature`(item: T, children: Array<T>) => T`
* that returns a new item, given an old item and it's new children.
* @return {Function} zipper factory with signature `(item: T) => Zipper`. The factory
* can also be accessed from the factory's `from` property.
*/
export function makeZipper(_isBranch, _getChildren, _makeItem) {
function makeConcreteZipper(item) {
return new Zipper(
item,
TOPPATH,
{
isBranch: _isBranch,
getChildren: _getChildren,
makeItem: _makeItem,
}
);
}
makeConcreteZipper.from = makeConcreteZipper;
return makeConcreteZipper;
}
// Export functions with arity > 1 curried
/**
* Inserts a new item as the left sibling.
*
* @function
* @param {T} item
* @param {Zipper} zipper
* @returns {Zipper}
*/
export const insertLeft = curry(_insertLeft);
/**
* Inserts a new item as the right sibling.
*
* @function
* @param {T} item
* @param {Zipper} zipper
* @returns {Zipper}
*/
export const insertRight = curry(_insertRight);
/**
* Inserts a new item as the leftmost child.
*
* @function
* @param {T} item
* @param {Zipper} zipper
* @returns {Zipper}
*/
export const insertChild = curry(_insertChild);
/**
* Inserts a new item as the rightmost child.
*
* @function
* @param {T} item
* @returns {Zipper}
*/
export const appendChild = curry(_appendChild);
/**
* Replaces the current item with value returned
* by calling `fn` with the current item.
*
* @function
* @param {Function} fn - Function that takes the old item
* and returns a new item.
* @param {Zipper} zipper
* @returns {Zipper}
*/
export const edit = curry(_edit);
/**
* Replaces the current item with the given value.
*
* @function
* @param {T} replaceWith - item to replace the current one with.
* @param {Zipper} zipper
* @returns {Zipper}
*/
export const replace = curry(_replace);
export default makeZipper;