whalesong/web-world/js-tree-cursor.js

312 lines
9.8 KiB
JavaScript

/*jslint vars: true, white: true, plusplus: true, maxerr: 50, indent: 4 */
// Offers functional views, traversals of the DOM and other tree-like structures.
// See Functional Pearl: The Zipper, by G\'erard Huet
// J. Functional Programming 7 (5): 549--554 Sepember 1997
var TreeCursor = (function() {
"use strict";
var TreeCursor = function(parent, node, prevs, nexts, openF, closeF, atomicF) {
this.parent = parent; // Parent can be the top (undefined), or a TreeCursor
this.node = node;
this.prevs = prevs;
this.nexts = nexts;
// openF: node -> (arrayof node)
this.openF = openF;
// closeF: node (arrayof node) -> node
// Given a node and its array of children, return a new node.
this.closeF = closeF;
// atomicF: node -> boolean
// Produces true if the node should be treated atomically.
this.atomicF = atomicF;
};
TreeCursor.prototype.isOnAtomicElement = function() {
return this.atomicF(this.node);
};
TreeCursor.prototype.canDown = function() {
return (!(this.atomicF(this.node)) &&
this.openF(this.node).length !== 0);
};
TreeCursor.prototype.down = function() {
if (this.atomicF(this.node)) {
throw new Error("down of atomic element");
}
var opened = this.openF(this.node);
if (opened.length === 0) {
throw new Error("down of empty");
}
return new TreeCursor(this,
opened[0],
[],
opened.slice(1),
this.openF,
this.closeF,
this.atomicF);
};
TreeCursor.prototype.canUp = function() {
return this.parent !== undefined;
};
TreeCursor.prototype.up = function() {
var parent = this.parent;
return new TreeCursor(parent.parent,
this.closeF(parent.node,
this.prevs.concat([this.node]).concat(this.nexts)),
parent.prevs,
parent.nexts,
this.openF,
this.closeF,
this.atomicF);
};
TreeCursor.prototype.canLeft = function() { return this.prevs.length !== 0; };
TreeCursor.prototype.left = function() {
if (this.prevs.length === 0) { throw new Error("left of first"); }
return new TreeCursor(this.parent,
this.prevs[this.prevs.length - 1],
this.prevs.slice(0, this.prevs.length - 1),
[this.node].concat(this.nexts),
this.openF,
this.closeF,
this.atomicF);
};
TreeCursor.prototype.canRight = function() { return this.nexts.length !== 0; };
TreeCursor.prototype.right = function() {
if (this.nexts.length === 0) { throw new Error("right of last"); }
return new TreeCursor(this.parent,
this.nexts[0],
this.prevs.concat([this.node]),
this.nexts.slice(1),
this.openF,
this.closeF,
this.atomicF);
};
TreeCursor.prototype.succ = function() {
var n;
if (this.canDown()) {
return this.down();
} else if (this.canRight()) {
return this.right();
} else {
n = this;
while (true) {
n = n.up();
if (n.canRight()) {
return n.right();
}
}
}
};
TreeCursor.prototype.pred = function() {
var n;
if (this.canLeft()) {
n = this.left();
while (n.canDown()) {
n = n.down();
while (n.canRight()) {
n = n.right();
}
}
return n;
} else {
return this.up();
}
};
TreeCursor.prototype.canPred = function() {
return this.canLeft() || this.canUp();
};
TreeCursor.prototype.canSucc = function() {
var n;
if (this.canDown()) {
return true;
} else if (this.canRight()) {
return true;
} else {
n = this;
while (true) {
if (! n.canUp()) { return false; }
n = n.up();
if (n.canRight()) {
return true;
}
}
}
};
TreeCursor.prototype.top = function() {
var n = this;
while (n.canUp()) { n = n.up(); }
return n;
};
//////////////////////////////////////////////////////////////////////
TreeCursor.prototype.replaceNode = function(n) {
return new TreeCursor(this.parent,
n,
this.prevs,
this.nexts,
this.openF,
this.closeF,
this.atomicF);
};
TreeCursor.prototype.insertRight = function(n) {
return new TreeCursor(this.parent,
n,
this.prevs.concat([this.node]),
this.nexts,
this.openF,
this.closeF,
this.atomicF);
};
TreeCursor.prototype.insertLeft = function(n) {
return new TreeCursor(this.parent,
n,
this.prevs,
[this.node].concat(this.nexts),
this.openF,
this.closeF,
this.atomicF);
};
TreeCursor.prototype.insertDown = function(n) {
if (this.atomicF(this.node)) {
throw new Error("down of atomic element");
}
return new TreeCursor(this,
n,
[],
this.openF(this.node),
this.openF,
this.closeF,
this.atomicF);
};
TreeCursor.prototype.deleteNode = function() {
var parent;
if (this.nexts.length !== 0) {
return new TreeCursor(this.parent,
this.nexts[0],
this.prevs,
this.nexts.slice(1),
this.openF,
this.closeF,
this.atomicF);
} else if (this.prevs.length !== 0) {
return new TreeCursor(this.parent,
this.prevs[this.prevs.length - 1],
this.prevs.slice(0, this.prevs.length - 1),
this.nexts,
this.openF,
this.closeF,
this.atomicF);
} else {
parent = this.parent;
return new TreeCursor(parent.parent,
this.closeF(parent.node, []),
parent.prevs,
parent.nexts,
this.openF,
this.closeF,
this.atomicF);
}
};
//////////////////////////////////////////////////////////////////////
TreeCursor.adaptTreeCursor = function(node, openF, closeF, atomicF) {
return new TreeCursor(undefined,
node,
[],
[],
openF,
closeF,
atomicF);
};
TreeCursor.arrayToCursor = function(anArray) {
var arrayOpenF = function(n) {
if (n instanceof Array) {
return n;
} else {
return [];
}
};
var arrayCloseF = function(n, children) {
if (n instanceof Array) {
return children;
} else {
return n;
}
};
var arrayAtomicF = function(n) {
return !(n instanceof Array);
};
return TreeCursor.adaptTreeCursor(anArray,
arrayOpenF,
arrayCloseF,
arrayAtomicF);
};
TreeCursor.domToCursor = function(dom) {
var domOpenF =
// To go down, just take the children.
function(n) {
return [].slice.call(n.childNodes, 0);
};
var domCloseF =
// To go back up, take the node, do a shallow cloning, and replace the children.
function(node, children) {
var i;
var newNode = node.cloneNode(false);
for (i = 0; i < children.length; i++) {
newNode.appendChild(children[i].cloneNode(true));
}
return newNode;
};
var domAtomicF =
function(node) {
return node.nodeType !== 1;
};
return TreeCursor.adaptTreeCursor(dom.cloneNode(true),
domOpenF,
domCloseF,
domAtomicF);
};
return TreeCursor;
}());