blob: 335dafac673fd951408e4f7b54e56367f4c69399 [file] [log] [blame]
/*************************************************
* Base classes of edit tree-related objects
*
* Only doing tree node manipulation via these
* adopt/ disown methods guarantees well-formedness
* of the tree.
************************************************/
// L = 'left'
// R = 'right'
//
// the contract is that they can be used as object properties
// and (-L) === R, and (-R) === L.
var L = -1;
var R = 1;
function prayDirection(dir) {
pray('a direction was passed', dir === L || dir === R);
}
/**
* Tiny extension of jQuery adding directionalized DOM manipulation methods.
*
* Funny how Pjs v3 almost just works with `jQuery.fn.init`.
*
* jQuery features that don't work on $:
* - jQuery.*, like jQuery.ajax, obviously (Pjs doesn't and shouldn't
* copy constructor properties)
*
* - jQuery(function), the shortcut for `jQuery(document).ready(function)`,
* because `jQuery.fn.init` is idiosyncratic and Pjs doing, essentially,
* `jQuery.fn.init.apply(this, arguments)` isn't quite right, you need:
*
* _.init = function(s, c) { jQuery.fn.init.call(this, s, c, $(document)); };
*
* if you actually give a shit (really, don't bother),
* see https://github.com/jquery/jquery/blob/1.7.2/src/core.js#L889
*
* - jQuery(selector), because jQuery translates that to
* `jQuery(document).find(selector)`, but Pjs doesn't (should it?) let
* you override the result of a constructor call
* + note that because of the jQuery(document) shortcut-ness, there's also
* the 3rd-argument-needs-to-be-`$(document)` thing above, but the fix
* for that (as can be seen above) is really easy. This problem requires
* a way more intrusive fix
*
* And that's it! Everything else just magically works because jQuery internally
* uses `this.constructor()` everywhere (hence calling `$`), but never ever does
* `this.constructor.find` or anything like that, always doing `jQuery.find`.
*/
var $ = P(jQuery, function(_) {
_.insDirOf = function(dir, el) {
return dir === L ?
this.insertBefore(el.first()) : this.insertAfter(el.last());
};
_.insAtDirEnd = function(dir, el) {
return dir === L ? this.prependTo(el) : this.appendTo(el);
};
});
var Point = P(function(_) {
_.parent = 0;
_[L] = 0;
_[R] = 0;
_.init = function(parent, leftward, rightward) {
this.parent = parent;
this[L] = leftward;
this[R] = rightward;
};
this.copy = function(pt) {
return Point(pt.parent, pt[L], pt[R]);
};
});
/**
* MathQuill virtual-DOM tree-node abstract base class
*/
var Node = P(function(_) {
_[L] = 0;
_[R] = 0
_.parent = 0;
var id = 0;
function uniqueNodeId() { return id += 1; }
this.byId = {};
_.init = function() {
this.id = uniqueNodeId();
Node.byId[this.id] = this;
this.ends = {};
this.ends[L] = 0;
this.ends[R] = 0;
};
_.dispose = function() { delete Node.byId[this.id]; };
_.toString = function() { return '{{ MathQuill Node #'+this.id+' }}'; };
_.jQ = $();
_.jQadd = function(jQ) { return this.jQ = this.jQ.add(jQ); };
_.jQize = function(jQ) {
// jQuery-ifies this.html() and links up the .jQ of all corresponding Nodes
var jQ = $(jQ || this.html());
function jQadd(el) {
if (el.getAttribute) {
var cmdId = el.getAttribute('mathquill-command-id');
var blockId = el.getAttribute('mathquill-block-id');
if (cmdId) Node.byId[cmdId].jQadd(el);
if (blockId) Node.byId[blockId].jQadd(el);
}
for (el = el.firstChild; el; el = el.nextSibling) {
jQadd(el);
}
}
for (var i = 0; i < jQ.length; i += 1) jQadd(jQ[i]);
return jQ;
};
_.createDir = function(dir, cursor) {
prayDirection(dir);
var node = this;
node.jQize();
node.jQ.insDirOf(dir, cursor.jQ);
cursor[dir] = node.adopt(cursor.parent, cursor[L], cursor[R]);
return node;
};
_.createLeftOf = function(el) { return this.createDir(L, el); };
_.selectChildren = function(leftEnd, rightEnd) {
return Selection(leftEnd, rightEnd);
};
_.bubble = iterator(function(yield_) {
for (var ancestor = this; ancestor; ancestor = ancestor.parent) {
var result = yield_(ancestor);
if (result === false) break;
}
return this;
});
_.postOrder = iterator(function(yield_) {
(function recurse(descendant) {
descendant.eachChild(recurse);
yield_(descendant);
})(this);
return this;
});
_.isEmpty = function() {
return this.ends[L] === 0 && this.ends[R] === 0;
};
_.isStyleBlock = function() {
return false;
};
_.children = function() {
return Fragment(this.ends[L], this.ends[R]);
};
_.eachChild = function() {
var children = this.children();
children.each.apply(children, arguments);
return this;
};
_.foldChildren = function(fold, fn) {
return this.children().fold(fold, fn);
};
_.withDirAdopt = function(dir, parent, withDir, oppDir) {
Fragment(this, this).withDirAdopt(dir, parent, withDir, oppDir);
return this;
};
_.adopt = function(parent, leftward, rightward) {
Fragment(this, this).adopt(parent, leftward, rightward);
return this;
};
_.disown = function() {
Fragment(this, this).disown();
return this;
};
_.remove = function() {
this.jQ.remove();
this.postOrder('dispose');
return this.disown();
};
});
function prayWellFormed(parent, leftward, rightward) {
pray('a parent is always present', parent);
pray('leftward is properly set up', (function() {
// either it's empty and `rightward` is the left end child (possibly empty)
if (!leftward) return parent.ends[L] === rightward;
// or it's there and its [R] and .parent are properly set up
return leftward[R] === rightward && leftward.parent === parent;
})());
pray('rightward is properly set up', (function() {
// either it's empty and `leftward` is the right end child (possibly empty)
if (!rightward) return parent.ends[R] === leftward;
// or it's there and its [L] and .parent are properly set up
return rightward[L] === leftward && rightward.parent === parent;
})());
}
/**
* An entity outside the virtual tree with one-way pointers (so it's only a
* "view" of part of the tree, not an actual node/entity in the tree) that
* delimits a doubly-linked list of sibling nodes.
* It's like a fanfic love-child between HTML DOM DocumentFragment and the Range
* classes: like DocumentFragment, its contents must be sibling nodes
* (unlike Range, whose contents are arbitrary contiguous pieces of subtrees),
* but like Range, it has only one-way pointers to its contents, its contents
* have no reference to it and in fact may still be in the visible tree (unlike
* DocumentFragment, whose contents must be detached from the visible tree
* and have their 'parent' pointers set to the DocumentFragment).
*/
var Fragment = P(function(_) {
_.init = function(withDir, oppDir, dir) {
if (dir === undefined) dir = L;
prayDirection(dir);
pray('no half-empty fragments', !withDir === !oppDir);
this.ends = {};
if (!withDir) return;
pray('withDir is passed to Fragment', withDir instanceof Node);
pray('oppDir is passed to Fragment', oppDir instanceof Node);
pray('withDir and oppDir have the same parent',
withDir.parent === oppDir.parent);
this.ends[dir] = withDir;
this.ends[-dir] = oppDir;
// To build the jquery collection for a fragment, accumulate elements
// into an array and then call jQ.add once on the result. jQ.add sorts the
// collection according to document order each time it is called, so
// building a collection by folding jQ.add directly takes more than
// quadratic time in the number of elements.
//
// https://github.com/jquery/jquery/blob/2.1.4/src/traversing.js#L112
var accum = this.fold([], function (accum, el) {
accum.push.apply(accum, el.jQ.get());
return accum;
});
this.jQ = this.jQ.add(accum);
};
_.jQ = $();
// like Cursor::withDirInsertAt(dir, parent, withDir, oppDir)
_.withDirAdopt = function(dir, parent, withDir, oppDir) {
return (dir === L ? this.adopt(parent, withDir, oppDir)
: this.adopt(parent, oppDir, withDir));
};
_.adopt = function(parent, leftward, rightward) {
prayWellFormed(parent, leftward, rightward);
var self = this;
self.disowned = false;
var leftEnd = self.ends[L];
if (!leftEnd) return this;
var rightEnd = self.ends[R];
if (leftward) {
// NB: this is handled in the ::each() block
// leftward[R] = leftEnd
} else {
parent.ends[L] = leftEnd;
}
if (rightward) {
rightward[L] = rightEnd;
} else {
parent.ends[R] = rightEnd;
}
self.ends[R][R] = rightward;
self.each(function(el) {
el[L] = leftward;
el.parent = parent;
if (leftward) leftward[R] = el;
leftward = el;
});
return self;
};
_.disown = function() {
var self = this;
var leftEnd = self.ends[L];
// guard for empty and already-disowned fragments
if (!leftEnd || self.disowned) return self;
self.disowned = true;
var rightEnd = self.ends[R]
var parent = leftEnd.parent;
prayWellFormed(parent, leftEnd[L], leftEnd);
prayWellFormed(parent, rightEnd, rightEnd[R]);
if (leftEnd[L]) {
leftEnd[L][R] = rightEnd[R];
} else {
parent.ends[L] = rightEnd[R];
}
if (rightEnd[R]) {
rightEnd[R][L] = leftEnd[L];
} else {
parent.ends[R] = leftEnd[L];
}
return self;
};
_.remove = function() {
this.jQ.remove();
this.each('postOrder', 'dispose');
return this.disown();
};
_.each = iterator(function(yield_) {
var self = this;
var el = self.ends[L];
if (!el) return self;
for (; el !== self.ends[R][R]; el = el[R]) {
var result = yield_(el);
if (result === false) break;
}
return self;
});
_.fold = function(fold, fn) {
this.each(function(el) {
fold = fn.call(this, fold, el);
});
return fold;
};
});
/**
* Registry of LaTeX commands and commands created when typing
* a single character.
*
* (Commands are all subclasses of Node.)
*/
var LatexCmds = {}, CharCmds = {};