UNPKG

zippa

Version:
979 lines (806 loc) 25.8 kB
'use strict'; Object.defineProperty(exports, "__esModule", { value: true }); exports.replace = exports.edit = exports.appendChild = exports.insertChild = exports.insertRight = exports.insertLeft = exports.prev = exports.next = exports.root = exports.up = exports.canGoUp = exports.canGoRight = exports.canGoLeft = exports.isRightmost = exports.isLeftmost = exports.isNotTop = exports.isTop = exports.isEnd = exports.getChildren = exports.isLeaf = exports.value = exports.whilst = undefined; exports.zipperFrom = zipperFrom; exports.isBranch = isBranch; exports.leftmost = leftmost; exports.left = left; exports.right = right; exports.rightmost = rightmost; exports.canGoDown = canGoDown; exports.down = down; exports.remove = remove; exports.makeZipper = makeZipper; var _ = require('ramda/src/__'); var _2 = _interopRequireDefault(_); var _assoc = require('ramda/src/assoc'); var _assoc2 = _interopRequireDefault(_assoc); var _of = require('ramda/src/of'); var _of2 = _interopRequireDefault(_of); var _always = require('ramda/src/always'); var _always2 = _interopRequireDefault(_always); var _call = require('ramda/src/call'); var _call2 = _interopRequireDefault(_call); var _cond = require('ramda/src/cond'); var _cond2 = _interopRequireDefault(_cond); var _converge = require('ramda/src/converge'); var _converge2 = _interopRequireDefault(_converge); var _complement = require('ramda/src/complement'); var _complement2 = _interopRequireDefault(_complement); var _curry = require('ramda/src/curry'); var _curry2 = _interopRequireDefault(_curry); var _either = require('ramda/src/either'); var _either2 = _interopRequireDefault(_either); var _head = require('ramda/src/head'); var _head2 = _interopRequireDefault(_head); var _equals = require('ramda/src/equals'); var _equals2 = _interopRequireDefault(_equals); var _identity = require('ramda/src/identity'); var _identity2 = _interopRequireDefault(_identity); var _init = require('ramda/src/init'); var _init2 = _interopRequireDefault(_init); var _ifElse = require('ramda/src/ifElse'); var _ifElse2 = _interopRequireDefault(_ifElse); var _last = require('ramda/src/last'); var _last2 = _interopRequireDefault(_last); var _or = require('ramda/src/or'); var _or2 = _interopRequireDefault(_or); var _prop = require('ramda/src/prop'); var _prop2 = _interopRequireDefault(_prop); var _juxt = require('ramda/src/juxt'); var _juxt2 = _interopRequireDefault(_juxt); var _tail = require('ramda/src/tail'); var _tail2 = _interopRequireDefault(_tail); var _pipe = require('ramda/src/pipe'); var _pipe2 = _interopRequireDefault(_pipe); var _merge = require('ramda/src/merge'); var _merge2 = _interopRequireDefault(_merge); var _T = require('ramda/src/T'); var _T2 = _interopRequireDefault(_T); var _when = require('ramda/src/when'); var _when2 = _interopRequireDefault(_when); var _unless = require('ramda/src/unless'); var _unless2 = _interopRequireDefault(_unless); var _until = require('ramda/src/until'); var _until2 = _interopRequireDefault(_until); var _unnest = require('ramda/src/unnest'); var _unnest2 = _interopRequireDefault(_unnest); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var TOP = null; var END = 'END'; function isEmpty(arr) { return !arr.length; } var whilst = exports.whilst = (0, _curry2.default)(function (predicate, fn, value) { var curr = value; while (predicate(curr)) { curr = fn(curr); } return curr; }); var 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; } var getItem = (0, _prop2.default)('item'); /** * Gets the value of the current location. * @param {Zipper} zipper * @returns {T|null} */ var value = exports.value = getItem; var getPath = (0, _prop2.default)('path'); var getMeta = (0, _prop2.default)('meta'); var sideEffect = (0, _curry2.default)(function (fn, x) { fn(x); return x; }); var _raiser = function _raiser(msg) { return function () { throw new Error(msg); }; }; var raise = (0, _pipe2.default)(_raiser, sideEffect); 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} */ 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} */ var isLeaf = exports.isLeaf = (0, _complement2.default)(isBranch); var _getChildrenFn = (0, _pipe2.default)(getMeta, (0, _prop2.default)('getChildren')); var getChildren = exports.getChildren = (0, _pipe2.default)((0, _when2.default)(isLeaf, raise('Tried getting children of a leaf')), (0, _converge2.default)(_call2.default, [_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} */ var isEnd = exports.isEnd = (0, _pipe2.default)(getPath, (0, _equals2.default)(END)); var _parentItemsFromPath = (0, _prop2.default)('parentItems'); var _parent = (0, _pipe2.default)(_parentItemsFromPath, _last2.default); var _parentPath = (0, _prop2.default)('parentPath'); var getParentItems = (0, _pipe2.default)(getPath, (0, _when2.default)((0, _equals2.default)(END), raise('Can\'t get parent items from end path.')), _parentItemsFromPath); var getParent = (0, _pipe2.default)(getPath, _parent); var getParentPath = (0, _pipe2.default)(getPath, _parentPath); /** * Returns a boolean indicating if the zipper is at the top. * @param {Zipper} zipper * @returns {boolean} */ var isTop = exports.isTop = (0, _pipe2.default)(getParentItems, (0, _equals2.default)(TOP)); /** * Returns a boolean indicating if the zipper is not at the top. * @param {Zipper} zipper * @returns {boolean} */ var isNotTop = exports.isNotTop = (0, _complement2.default)(isTop); var _leftsFromPath = (0, _pipe2.default)((0, _prop2.default)('left'), (0, _or2.default)(_2.default, [])); var _rightsFromPath = (0, _pipe2.default)((0, _prop2.default)('right'), (0, _or2.default)(_2.default, [])); var lefts = (0, _pipe2.default)(getPath, _leftsFromPath); var rights = (0, _pipe2.default)(getPath, _rightsFromPath); var hasChanged = (0, _pipe2.default)(getPath, (0, _prop2.default)('changed'), Boolean); var isUnchanged = (0, _complement2.default)(hasChanged); var isNotEmpty = (0, _complement2.default)(isEmpty); /** * Returns a boolean indicating if the item at the current location * is the leftmost sibling. * @param {Zipper} zipper * @returns {boolean} */ var isLeftmost = exports.isLeftmost = (0, _pipe2.default)(lefts, isEmpty); /** * Returns a boolean indicating if the item at the current location * is the rightmost sibling. * @param {Zipper} zipper * @returns {boolean} */ var isRightmost = exports.isRightmost = (0, _pipe2.default)(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} */ var canGoLeft = exports.canGoLeft = (0, _complement2.default)(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} */ var canGoRight = exports.canGoRight = (0, _complement2.default)(isRightmost); /** * Moves location to the leftmost sibling. * If the current location is already the leftmost, * returns itself. * * @param {Zipper} zipper * @returns {Zipper} */ function leftmost(zipper) { if (isTop(zipper) || isLeftmost(zipper)) return zipper; var path = getPath(zipper); var _lefts = _leftsFromPath(path); var _rights = _rightsFromPath(path); var item = getItem(zipper); var leftMost = (0, _head2.default)(_lefts); var newLeft = []; var newRight = (0, _tail2.default)(_lefts).concat([item], _rights); return zipperFrom(zipper, leftMost, (0, _merge2.default)(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} */ function left(zipper) { if (isEnd(zipper)) return zipper; if (isLeftmost(zipper)) return null; var item = getItem(zipper); var path = getPath(zipper); var _lefts = _leftsFromPath(path); var _rights = _rightsFromPath(path); var leftSibling = (0, _last2.default)(_lefts); var newLeft = (0, _init2.default)(_lefts); var newRight = [item].concat(_rights); return zipperFrom(zipper, leftSibling, (0, _merge2.default)(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} */ function right(zipper) { if (isEnd(zipper)) return zipper; if (isRightmost(zipper)) return null; var item = getItem(zipper); var path = getPath(zipper); var _lefts = _leftsFromPath(path); var _rights = _rightsFromPath(path); var rightSibling = (0, _head2.default)(_rights); var newLeft = _lefts.concat([item]); var newRight = (0, _tail2.default)(_rights); return zipperFrom(zipper, rightSibling, (0, _merge2.default)(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} */ function rightmost(zipper) { if (isRightmost(zipper)) return zipper; var path = getPath(zipper); var _rights = _rightsFromPath(path); var _lefts = _leftsFromPath(path); var item = getItem(zipper); var rightMost = (0, _last2.default)(_rights); var newLeft = _lefts.concat([item], (0, _init2.default)(_rights)); var newRight = []; return zipperFrom(zipper, rightMost, (0, _merge2.default)(path, { left: newLeft, right: newRight })); } /** * Alias for `isBranch` * * @param {Zipper} zipper * @returns {boolean} */ 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} */ function down(zipper) { if (!isBranch(zipper)) return null; var item = getItem(zipper); var path = getPath(zipper); var children = getChildren(zipper); var newLeft = []; var newRight = (0, _tail2.default)(children); var newLoc = zipperFrom(zipper, (0, _head2.default)(children), (0, _merge2.default)(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'); } var item = getItem(zipper); var path = getPath(zipper); var _lefts = _leftsFromPath(path); return zipperFrom(zipper, item, (0, _merge2.default)(path, { left: _lefts.concat([insertItem]), changed: true })); } function _insertRight(insertItem, zipper) { if (isTop(zipper)) { throw new Error('Tried inserting left of top'); } var item = getItem(zipper); var path = getPath(zipper); var _rights = _rightsFromPath(path); return zipperFrom(zipper, item, (0, _merge2.default)(path, { right: [insertItem].concat(_rights), changed: true })); } function _replace(_replaceWith, zipper) { if (getItem(zipper) === _replaceWith) return zipper; return zipperFrom(zipper, _replaceWith, (0, _assoc2.default)('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} */ var canGoUp = exports.canGoUp = isNotTop; var _unchangedUp = (0, _converge2.default)(zipperFrom, [_identity2.default, getParent, getParentPath]); var itemsOnCurrentLevel = (0, _pipe2.default)((0, _juxt2.default)([lefts, (0, _pipe2.default)(getItem, _of2.default), rights]), _unnest2.default); var makeParentItem = (0, _converge2.default)(makeItem, [_identity2.default, 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} */ var up = exports.up = (0, _cond2.default)([[(0, _or2.default)(isTop, isEnd), (0, _always2.default)(null)], [isUnchanged, _unchangedUp], [_T2.default, (0, _converge2.default)(zipperFrom, [_identity2.default, makeParentItem, (0, _pipe2.default)(getParentPath, (0, _assoc2.default)('changed', true))])]]); /** * Moves location to the root, constructing * any changes made. * * @param {Zipper} zipper * @returns {Zipper} */ var root = exports.root = (0, _unless2.default)(isEnd, whilst(isNotTop, up)); var toEnd = function toEnd(z) { return zipperFrom(z, getItem(z), END); }; var nextUp = (0, _pipe2.default)((0, _until2.default)((0, _either2.default)(isTop, canGoRight), up), (0, _ifElse2.default)(isTop, toEnd, right)); /** * Moves location to the next element in depth-first order. * * @param {Zipper} zipper * @returns {Zipper} */ var next = exports.next = (0, _cond2.default)([[isEnd, _identity2.default], [canGoDown, down], [canGoRight, right], [_T2.default, nextUp]]); var _goToRightmostChild = (0, _pipe2.default)(down, rightmost); /** * Moves location to the previous element in depth-first order. * * @param {Zipper} zipper * @returns {Zipper} */ var prev = exports.prev = (0, _ifElse2.default)(canGoLeft, (0, _pipe2.default)(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} */ function remove(zipper) { if (isTop(zipper)) { throw new Error('Can\'t remove top.'); } if (isEnd(zipper)) throw new Error('Can\'t remove end'); var path = getPath(zipper); var _lefts = _leftsFromPath(path); if (_lefts.length) { var leftSibling = zipperFrom(zipper, (0, _last2.default)(_lefts), (0, _merge2.default)(path, { left: (0, _init2.default)(_lefts), changed: true })); return whilst(isBranch, _goToRightmostChild, leftSibling); } else { var _rights = _rightsFromPath(path); var parentPath = _parentPath(path); var parent = _parent(path); return zipperFrom(zipper, makeItem(zipper, parent, _rights), isTop(zipper) ? parentPath : (0, _assoc2.default)('changed', true, parentPath)); } } function _insertChild(item, z) { var newChildren = [item].concat(getChildren(z)); return _replace(makeItem(z, item, newChildren), z); } function _appendChild(item, z) { var 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. */ 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} */ var insertLeft = exports.insertLeft = (0, _curry2.default)(_insertLeft); /** * Inserts a new item as the right sibling. * * @function * @param {T} item * @param {Zipper} zipper * @returns {Zipper} */ var insertRight = exports.insertRight = (0, _curry2.default)(_insertRight); /** * Inserts a new item as the leftmost child. * * @function * @param {T} item * @param {Zipper} zipper * @returns {Zipper} */ var insertChild = exports.insertChild = (0, _curry2.default)(_insertChild); /** * Inserts a new item as the rightmost child. * * @function * @param {T} item * @returns {Zipper} */ var appendChild = exports.appendChild = (0, _curry2.default)(_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} */ var edit = exports.edit = (0, _curry2.default)(_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} */ var replace = exports.replace = (0, _curry2.default)(_replace); exports.default = makeZipper;