ngx-mathquill
Version:
A thin typescript wrapper around mathquill
1,536 lines (1,334 loc) • 160 kB
JavaScript
/**
* MathQuill v0.10.1, by Han, Jeanine, and Mary
* http://mathquill.com | maintainers@mathquill.com
*
* This Source Code Form is subject to the terms of the
* Mozilla Public License, v. 2.0. If a copy of the MPL
* was not distributed with this file, You can obtain
* one at http://mozilla.org/MPL/2.0/.
*/
(function() {
var jQuery = window.jQuery,
undefined,
mqCmdId = 'mathquill-command-id',
mqBlockId = 'mathquill-block-id',
min = Math.min,
max = Math.max;
if (!jQuery) throw 'MathQuill requires jQuery 1.5.2+ to be loaded first';
function noop() {}
/**
* A utility higher-order function that makes defining variadic
* functions more convenient by letting you essentially define functions
* with the last argument as a splat, i.e. the last argument "gathers up"
* remaining arguments to the function:
* var doStuff = variadic(function(first, rest) { return rest; });
* doStuff(1, 2, 3); // => [2, 3]
*/
var __slice = [].slice;
function variadic(fn) {
var numFixedArgs = fn.length - 1;
return function() {
var args = __slice.call(arguments, 0, numFixedArgs);
var varArg = __slice.call(arguments, numFixedArgs);
return fn.apply(this, args.concat([ varArg ]));
};
}
/**
* A utility higher-order function that makes combining object-oriented
* programming and functional programming techniques more convenient:
* given a method name and any number of arguments to be bound, returns
* a function that calls it's first argument's method of that name (if
* it exists) with the bound arguments and any additional arguments that
* are passed:
* var sendMethod = send('method', 1, 2);
* var obj = { method: function() { return Array.apply(this, arguments); } };
* sendMethod(obj, 3, 4); // => [1, 2, 3, 4]
* // or more specifically,
* var obj2 = { method: function(one, two, three) { return one*two + three; } };
* sendMethod(obj2, 3); // => 5
* sendMethod(obj2, 4); // => 6
*/
var send = variadic(function(method, args) {
return variadic(function(obj, moreArgs) {
if (method in obj) return obj[method].apply(obj, args.concat(moreArgs));
});
});
/**
* A utility higher-order function that creates "implicit iterators"
* from "generators": given a function that takes in a sole argument,
* a "yield_" function, that calls "yield_" repeatedly with an object as
* a sole argument (presumably objects being iterated over), returns
* a function that calls it's first argument on each of those objects
* (if the first argument is a function, it is called repeatedly with
* each object as the first argument, otherwise it is stringified and
* the method of that name is called on each object (if such a method
* exists)), passing along all additional arguments:
* var a = [
* { method: function(list) { list.push(1); } },
* { method: function(list) { list.push(2); } },
* { method: function(list) { list.push(3); } }
* ];
* a.each = iterator(function(yield_) {
* for (var i in this) yield_(this[i]);
* });
* var list = [];
* a.each('method', list);
* list; // => [1, 2, 3]
* // Note that the for-in loop will yield 'each', but 'each' maps to
* // the function object created by iterator() which does not have a
* // .method() method, so that just fails silently.
*/
function iterator(generator) {
return variadic(function(fn, args) {
if (typeof fn !== 'function') fn = send(fn);
var yield_ = function(obj) { return fn.apply(obj, [ obj ].concat(args)); };
return generator.call(this, yield_);
});
}
/**
* sugar to make defining lots of commands easier.
* TODO: rethink this.
*/
function bind(cons /*, args... */) {
var args = __slice.call(arguments, 1);
return function() {
return cons.apply(this, args);
};
}
/**
* a development-only debug method. This definition and all
* calls to `pray` will be stripped from the minified
* build of mathquill.
*
* This function must be called by name to be removed
* at compile time. Do not define another function
* with the same name, and only call this function by
* name.
*/
function pray(message, cond) {
if (!cond) throw new Error('prayer failed: '+message);
}
var P = (function(prototype, ownProperty, undefined) {
// helper functions that also help minification
function isObject(o) { return typeof o === 'object'; }
function isFunction(f) { return typeof f === 'function'; }
// used to extend the prototypes of superclasses (which might not
// have `.Bare`s)
function SuperclassBare() {}
return function P(_superclass /* = Object */, definition) {
// handle the case where no superclass is given
if (definition === undefined) {
definition = _superclass;
_superclass = Object;
}
// C is the class to be returned.
//
// It delegates to instantiating an instance of `Bare`, so that it
// will always return a new instance regardless of the calling
// context.
//
// TODO: the Chrome inspector shows all created objects as `C`
// rather than `Object`. Setting the .name property seems to
// have no effect. Is there a way to override this behavior?
function C() {
var self = new Bare;
if (isFunction(self.init)) self.init.apply(self, arguments);
return self;
}
// C.Bare is a class with a noop constructor. Its prototype is the
// same as C, so that instances of C.Bare are also instances of C.
// New objects can be allocated without initialization by calling
// `new MyClass.Bare`.
function Bare() {}
C.Bare = Bare;
// Set up the prototype of the new class.
var _super = SuperclassBare[prototype] = _superclass[prototype];
var proto = Bare[prototype] = C[prototype] = C.p = new SuperclassBare;
// other variables, as a minifier optimization
var extensions;
// set the constructor property on the prototype, for convenience
proto.constructor = C;
C.extend = function(def) { return P(C, def); }
return (C.open = function(def) {
extensions = {};
if (isFunction(def)) {
// call the defining function with all the arguments you need
// extensions captures the return value.
extensions = def.call(C, proto, _super, C, _superclass);
}
else if (isObject(def)) {
// if you passed an object instead, we'll take it
extensions = def;
}
// ...and extend it
if (isObject(extensions)) {
for (var ext in extensions) {
if (ownProperty.call(extensions, ext)) {
proto[ext] = extensions[ext];
}
}
}
// if there's no init, we assume we're inheriting a non-pjs class, so
// we default to applying the superclass's constructor.
if (!isFunction(proto.init)) {
proto.init = _superclass;
}
return C;
})(definition);
}
// as a minifier optimization, we've closured in a few helper functions
// and the string 'prototype' (C[p] is much shorter than C.prototype)
})('prototype', ({}).hasOwnProperty);
/*************************************************
* 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 = {};
/********************************************
* Cursor and Selection "singleton" classes
*******************************************/
/* The main thing that manipulates the Math DOM. Makes sure to manipulate the
HTML DOM to match. */
/* Sort of singletons, since there should only be one per editable math
textbox, but any one HTML document can contain many such textboxes, so any one
JS environment could actually contain many instances. */
//A fake cursor in the fake textbox that the math is rendered in.
var Cursor = P(Point, function(_) {
_.init = function(initParent, options) {
this.parent = initParent;
this.options = options;
var jQ = this.jQ = this._jQ = $('<span class="mq-cursor">​</span>');
//closured for setInterval
this.blink = function(){ jQ.toggleClass('mq-blink'); };
this.upDownCache = {};
};
_.show = function() {
this.jQ = this._jQ.removeClass('mq-blink');
if ('intervalId' in this) //already was shown, just restart interval
clearInterval(this.intervalId);
else { //was hidden and detached, insert this.jQ back into HTML DOM
if (this[R]) {
if (this.selection && this.selection.ends[L][L] === this[L])
this.jQ.insertBefore(this.selection.jQ);
else
this.jQ.insertBefore(this[R].jQ.first());
}
else
this.jQ.appendTo(this.parent.jQ);
this.parent.focus();
}
this.intervalId = setInterval(this.blink, 500);
return this;
};
_.hide = function() {
if ('intervalId' in this)
clearInterval(this.intervalId);
delete this.intervalId;
this.jQ.detach();
this.jQ = $();
return this;
};
_.withDirInsertAt = function(dir, parent, withDir, oppDir) {
var oldParent = this.parent;
this.parent = parent;
this[dir] = withDir;
this[-dir] = oppDir;
// by contract, .blur() is called after all has been said and done
// and the cursor has actually been moved
// FIXME pass cursor to .blur() so text can fix cursor pointers when removing itself
if (oldParent !== parent && oldParent.blur) oldParent.blur(this);
};
_.insDirOf = function(dir, el) {
prayDirection(dir);
this.jQ.insDirOf(dir, el.jQ);
this.withDirInsertAt(dir, el.parent, el[dir], el);
this.parent.jQ.addClass('mq-hasCursor');
return this;
};
_.insLeftOf = function(el) { return this.insDirOf(L, el); };
_.insRightOf = function(el) { return this.insDirOf(R, el); };
_.insAtDirEnd = function(dir, el) {
prayDirection(dir);
this.jQ.insAtDirEnd(dir, el.jQ);
this.withDirInsertAt(dir, el, 0, el.ends[dir]);
el.focus();
return this;
};
_.insAtLeftEnd = function(el) { return this.insAtDirEnd(L, el); };
_.insAtRightEnd = function(el) { return this.insAtDirEnd(R, el); };
/**
* jump up or down from one block Node to another:
* - cache the current Point in the node we're jumping from
* - check if there's a Point in it cached for the node we're jumping to
* + if so put the cursor there,
* + if not seek a position in the node that is horizontally closest to
* the cursor's current position
*/
_.jumpUpDown = function(from, to) {
var self = this;
self.upDownCache[from.id] = Point.copy(self);
var cached = self.upDownCache[to.id];
if (cached) {
cached[R] ? self.insLeftOf(cached[R]) : self.insAtRightEnd(cached.parent);
}
else {
var pageX = self.offset().left;
to.seek(pageX, self);
}
};
_.offset = function() {
//in Opera 11.62, .getBoundingClientRect() and hence jQuery::offset()
//returns all 0's on inline elements with negative margin-right (like
//the cursor) at the end of their parent, so temporarily remove the
//negative margin-right when calling jQuery::offset()
//Opera bug DSK-360043
//http://bugs.jquery.com/ticket/11523
//https://github.com/jquery/jquery/pull/717
var self = this, offset = self.jQ.removeClass('mq-cursor').offset();
self.jQ.addClass('mq-cursor');
return offset;
}
_.unwrapGramp = function() {
var gramp = this.parent.parent;
var greatgramp = gramp.parent;
var rightward = gramp[R];
var cursor = this;
var leftward = gramp[L];
gramp.disown().eachChild(function(uncle) {
if (uncle.isEmpty()) return;
uncle.children()
.adopt(greatgramp, leftward, rightward)
.each(function(cousin) {
cousin.jQ.insertBefore(gramp.jQ.first());
})
;
leftward = uncle.ends[R];
});
if (!this[R]) { //then find something to be rightward to insLeftOf
if (this[L])
this[R] = this[L][R];
else {
while (!this[R]) {
this.parent = this.parent[R];
if (this.parent)
this[R] = this.parent.ends[L];
else {
this[R] = gramp[R];
this.parent = greatgramp;
break;
}
}
}
}
if (this[R])
this.insLeftOf(this[R]);
else
this.insAtRightEnd(greatgramp);
gramp.jQ.remove();
if (gramp[L].siblingDeleted) gramp[L].siblingDeleted(cursor.options, R);
if (gramp[R].siblingDeleted) gramp[R].siblingDeleted(cursor.options, L);
};
_.startSelection = function() {
var anticursor = this.anticursor = Point.copy(this);
var ancestors = anticursor.ancestors = {}; // a map from each ancestor of
// the anticursor, to its child that is also an ancestor; in other words,
// the anticursor's ancestor chain in reverse order
for (var ancestor = anticursor; ancestor.parent; ancestor = ancestor.parent) {
ancestors[ancestor.parent.id] = ancestor;
}
};
_.endSelection = function() {
delete this.anticursor;
};
_.select = function() {
var anticursor = this.anticursor;
if (this[L] === anticursor[L] && this.parent === anticursor.parent) return false;
// Find the lowest common ancestor (`lca`), and the ancestor of the cursor
// whose parent is the LCA (which'll be an end of the selection fragment).
for (var ancestor = this; ancestor.parent; ancestor = ancestor.parent) {
if (ancestor.parent.id in anticursor.ancestors) {
var lca = ancestor.parent;
break;
}
}
pray('cursor and anticursor in the same tree', lca);
// The cursor and the anticursor should be in the same tree, because the
// mousemove handler attached to the document, unlike the one attached to
// the root HTML DOM element, doesn't try to get the math tree node of the
// mousemove target, and Cursor::seek() based solely on coordinates stays
// within the tree of `this` cursor's root.
// The other end of the selection fragment, the ancestor of the anticursor
// whose parent is the LCA.
var antiAncestor = anticursor.ancestors[lca.id];
// Now we have two either Nodes or Points, guaranteed to have a common
// parent and guaranteed that if both are Points, they are not the same,
// and we have to figure out which is the left end and which the right end
// of the selection.
var leftEnd, rightEnd, dir = R;
// This is an extremely subtle algorithm.
// As a special case, `ancestor` could be a Point and `antiAncestor` a Node
// immediately to `ancestor`'s left.
// In all other cases,
// - both Nodes
// - `ancestor` a Point and `antiAncestor` a Node
// - `ancestor` a Node and `antiAncestor` a Point
// `antiAncestor[R] === rightward[R]` for some `rightward` that is
// `ancestor` or to its right, if and only if `antiAncestor` is to
// the right of `ancestor`.
if (ancestor[L] !== antiAncestor) {
for (var rightward = ancestor; rightward; rightward = rightward[R]) {
if (rightward[R] === antiAncestor[R]) {
dir = L;
leftEnd = ancestor;
rightEnd = antiAncestor;
break;
}
}
}
if (dir === R) {
leftEnd = antiAncestor;
rightEnd = ancestor;
}
// only want to select Nodes up to Points, can't select Points themselves
if (leftEnd instanceof Point) leftEnd = leftEnd[R];
if (rightEnd instanceof Point) rightEnd = rightEnd[L];
this.hide().selection = lca.selectChildren(leftEnd, rightEnd);
this.insDirOf(dir, this.selection.ends[dir]);
this.selectionChanged();
return true;
};
_.clearSelection = function() {
if (this.selection) {
this.selection.clear();
delete this.selection;
this.selectionChanged();
}
return this;
};
_.deleteSelection = function() {
if (!this.selection) return;
this[L] = this.selection.ends[L][L];
this[R] = this.selection.ends[R][R];
this.selection.remove();
this.selectionChanged();
delete this.selection;
};
_.replaceSelection = function() {
var seln = this.selection;
if (seln) {
this[L] = seln.ends[L][L];
this[R] = seln.ends[R][R];
delete this.selection;
}
return seln;
};
});
var Selection = P(Fragment, function(_, super_) {
_.init = function() {
super_.init.apply(this, arguments);
this.jQ = this.jQ.wrapAll('<span class="mq-selection"></span>').parent();
//can't do wrapAll(this.jQ = $(...)) because wrapAll will clone it
};
_.adopt = function() {
this.jQ.replaceWith(this.jQ = this.jQ.children());
return super_.adopt.apply(this, arguments);
};
_.clear = function() {
// using the browser's native .childNodes property so that we
// don't discard text nodes.
this.jQ.replaceWith(this.jQ[0].childNodes);
return this;
};
_.join = function(methodName) {
return this.fold('', function(fold, child) {
return fold + child[methodName]();
});
};
});
/*********************************************
* Controller for a MathQuill instance,
* on which services are registered with
*
* Controller.open(function(_) { ... });
*
********************************************/
var Controller = P(function(_) {
_.init = function(root, container, options) {
this.id = root.id;
this.data = {};
this.root = root;
this.container = container;
this.options = options;
root.controller = this;
this.cursor = root.cursor = Cursor(root, options);
// TODO: stop depending on root.cursor, and rm it
};
_.handle = function(name, dir) {
var handlers = this.options.handlers;
if (handlers && handlers.fns[name]) {
var mq = handlers.APIClasses[this.KIND_OF_MQ](this);
if (dir === L || dir === R) handlers.fns[name](dir, mq);
else handlers.fns[name](mq);
}
};
var notifyees = [];
this.onNotify = function(f) { notifyees.push(f); };
_.notify = function() {
for (var i = 0; i < notifyees.length; i += 1) {
notifyees[i].apply(this.cursor, arguments);
}
return this;
};
});
/*********************************************************
* The publicly exposed MathQuill API.
********************************************************/
var API = {}, Options = P(), optionProcessors = {}, Progenote = P(), EMBEDS = {};
/**
* Interface Versioning (#459, #495) to allow us to virtually guarantee
* backcompat. v0.10.x introduces it, so for now, don't completely break the
* API for people who don't know about it, just complain with console.warn().
*
* The methods are shimmed in outro.js so that MQ.MathField.prototype etc can
* be accessed.
*/
function insistOnInterVer() {
if (window.console) console.warn(
'You are using the MathQuill API without specifying an interface version, ' +
'which will fail in v1.0.0. Easiest fix is to do the following before ' +
'doing anything else:\n' +
'\n' +
' MathQuill = MathQuill.getInterface(1);\n' +
' // now MathQuill.MathField() works like it used to\n' +
'\n' +
'See also the "`dev` branch (2014\u20132015) \u2192 v0.10.0 Migration Guide" at\n' +
' https://github.com/mathquill/mathquill/wiki/%60dev%60-branch-(2014%E2%80%932015)-%E2%86%92-v0.10.0-Migration-Guide'
);
}
// globally exported API object
function MathQuill(el) {
insistOnInterVer();
return MQ1(el);
};
MathQuill.prototype = Progenote.p;
MathQuill.VERSION = "v0.10.1";
MathQuill.interfaceVersion = function(v) {
// shim for #459-era interface versioning (ended with #495)
if (v !== 1) throw 'Only interface version 1 supported. You specified: ' + v;
insistOnInterVer = function() {
if (window.console) console.warn(
'You called MathQuill.interfaceVersion(1); to specify the interface ' +
'version, which will fail in v1.0.0. You can fix this easily by doing ' +
'this before doing anything else:\n' +
'\n' +
' MathQuill = MathQuill.getInterface(1);\n' +
' // now MathQuill.MathField() works like it used to\n' +
'\n' +
'See also the "`dev` branch (2014\u20132015) \u2192 v0.10.0 Migration Guide" at\n' +
' https://github.com/mathquill/mathquill/wiki/%60dev%60-branch-(2014%E2%80%932015)-%E2%86%92-v0.10.0-Migration-Guide'
);
};
insistOnInterVer();
return MathQuill;
};
MathQuill.getInterface = getInterface;
var MIN = getInterface.MIN = 1, MAX = getInterface.MAX = 2;
function getInterface(v) {
if (!(MIN <= v && v <= MAX)) throw 'Only interface versions between ' +
MIN + ' and ' + MAX + ' supported. You specified: ' + v;
/**
* Function that takes an HTML element and, if it's the root HTML element of a
* static math or math or text field, returns an API object for it (else, null).
*
* var mathfield = MQ.MathField(mathFieldSpan);
* assert(MQ(mathFieldSpan).id === mathfield.id);
* assert(MQ(mathFieldSpan).id === MQ(mathFieldSpan).id);
*
*/
function MQ(el) {
if (!el || !el.nodeType) return null; // check that `el` is a HTML element, using the
// same technique as jQuery: https://github.com/jquery/jquery/blob/679536ee4b7a92ae64a5f58d90e9cc38c001e807/src/core/init.js#L92
var blockId = $(el).children('.mq-root-block').attr(mqBlockId);
var ctrlr = blockId && Node.byId[blockId].controller;
return ctrlr ? APIClasses[ctrlr.KIND_OF_MQ](ctrlr) : null;
};
var APIClasses = {};
MQ.L = L;
MQ.R = R;
MQ.saneKeyboardEvents = saneKeyboardEvents;
function config(currentOptions, newOptions) {
if (newOptions && newOptions.handlers) {
newOptions.handlers = { fns: newOptions.handlers, APIClasses: APIClasses };
}
for (var name in newOptions) if (newOptions.hasOwnProperty(name)) {
var value = newOptions[name], processor = optionProcessors[name];
currentOptions[name] = (processor ? processor(value) : value);
}
}
MQ.config = function(opts) { config(Options.p, opts); return this; };
MQ.registerEmbed = function(name, options) {
if (!/^[a-z][a-z0-9]*$/i.test(name)) {
throw 'Embed name must start with letter and be only letters and digits';
}
EMBEDS[name] = options;
};
var AbstractMathQuill = APIClasses.AbstractMathQuill = P(Progenote, function(_) {
_.init = function(ctrlr) {
this.__controller = ctrlr;
this.__options = ctrlr.options;
this.id = ctrlr.id;
this.data = ctrlr.data;
};
_.__mathquillify = function(classNames) {
var ctrlr = this.__controller, root = ctrlr.root, el = ctrlr.container;
ctrlr.createTextarea();
var contents = el.addClass(classNames).contents().detach();
root.jQ =
$('<span class="mq-root-block"/>').attr(mqBlockId, root.id).appendTo(el);
this.latex(contents.text());
this.revert = function() {
return el.empty().unbind('.mathquill')
.removeClass('mq-editable-field mq-math-mode mq-text-mode')
.append(contents);
};
};
_.config = function(opts) { config(this.__options, opts); return this; };
_.el = function() { return this.__controller.container[0]; };
_.text = function() { return this.__controller.exportText(); };
_.latex = function(latex) {
if (arguments.length > 0) {
this.__controller.renderLatexMath(latex);
if (this.__controller.blurred) this.__controller.cursor.hide().parent.blur();
return this;
}
return this.__controller.exportLatex();
};
_.html = function() {
return this.__controller.root.jQ.html()
.replace(/ mathquill-(?:command|block)-id="?\d+"?/g, '')
.replace(/<span class="?mq-cursor( mq-blink)?"?>.?<\/span>/i, '')
.replace(/ mq-hasCursor|mq-hasCursor ?/, '')
.replace(/ class=(""|(?= |>))/g, '');
};
_.reflow = function() {
this.__controller.root.postOrder('reflow');
return this;
};
});
MQ.prototype = AbstractMathQuill.prototype;
APIClasses.EditableField = P(AbstractMathQuill, function(_, super_) {
_.__mathquillify = function() {
super_.__mathquillify.apply(this, arguments);
this.__controller.editable = true;
this.__controller.delegateMouseEvents();
this.__controller.editablesTextareaEvents();
return this;
};
_.focus = function() { this.__controller.textarea.focus(); return this; };
_.blur = function() { this.__controller.textarea.blur(); return this; };
_.write = function(latex) {
this.__controller.writeLatex(latex);
this.__controller.scrollHoriz();
if (this.__controller.blurred) this.__controller.cursor.hide().parent.blur();
return this;
};
_.cmd = function(cmd) {
var ctrlr = this.__controller.notify(), cursor = ctrlr.cursor;
if (/^\\[a-z]+$/i.test(cmd)) {
cmd = cmd.slice(1);
var klass = LatexCmds[cmd] || Environments[cmd];
if (klass) {
cmd = klass(cmd);
if (cursor.selection) cmd.replaces(cursor.replaceSelection());
cmd.createLeftOf(cursor.show());
this.__controller.scrollHoriz();
}
else /* TODO: API needs better error reporting */;
}
else cursor.parent.write(cursor, cmd);
if (ctrlr.blurred) cursor.hide().parent.blur();
return this;
};
_.select = function() {
var ctrlr = this.__controller;
ctrlr.notify('move').cursor.insAtRightEnd(ctrlr.root);
while (ctrlr.cursor[L]) ctrlr.selectLeft();
return this;
};
_.clearSelection = function() {
this.__controller.cursor.clearSelection();
return this;
};
_.moveToDirEnd = function(dir) {
this.__controller.notify('move').cursor.insAtDirEnd(dir, this.__controller.root);
return this;
};
_.moveToLeftEnd = function() { return this.moveToDirEnd(L); };
_.moveToRightEnd = function() { return this.moveToDirEnd(R); };
_.keystroke = function(keys) {
var keys = keys.replace(/^\s+|\s+$/g, '').split(/\s+/);
for (var i = 0; i < keys.length; i += 1) {
this.__controller.keystroke(keys[i], { preventDefault: noop });
}
return this;
};
_.typedText = function(text) {
for (var i = 0; i < text.length; i += 1) this.__controller.typedText(text.charAt(i));
return this;
};
_.dropEmbedded = function(pageX, pageY, options) {
var clientX = pageX - $(window).scrollLeft();
var clientY = pageY - $(window).scrollTop();
var el = document.elementFromPoint(clientX, clientY);
this.__controller.seek($(el), pageX, pageY);
var cmd = Embed().setOptions(options);
cmd.createLeftOf(this.__controller.cursor);
};
_.clickAt = function(clientX, clientY, target) {
target = target || document.elementFromPoint(clientX, clientY);
var ctrlr = this.__controller, root = ctrlr.root;
if (!jQuery.contains(root.jQ[0], target)) target = root.jQ[0];
ctrlr.seek($(target), clientX + pageXOffset, clientY + pageYOffset);
if (ctrlr.blurred) this.focus();
return this;
};
_.ignoreNextMousedown = function(fn) {
this.__controller.cursor.options.ignoreNextMousedown = fn;
return this;
};
});
MQ.EditableField = function() { throw "wtf don't call me, I'm 'abstract'"; };
MQ.EditableField.prototype = APIClasses.EditableField.prototype;
/**
* Export the API functions that MathQuill-ify an HTML element into API objects
* of each class. If the element had already been MathQuill-ified but into a
* different kind (or it's not an HTML element), return null.
*/
for (var kind in API) (function(kind, defAPIClass) {
var APIClass = APIClasses[kind] = defAPIClass(APIClasses);
MQ[kind] = function(el, opts) {
var mq = MQ(el);
if (mq instanceof APIClass || !el || !el.nodeType) return mq;
var ctrlr = Controller(APIClass.RootBlock(), $(el), Options());
ctrlr.KIND_OF_MQ = kind;
return APIClass(ctrlr).__mathquillify(opts, v);
};
MQ[kind].prototype = APIClass.prototype;
}(kind, API[kind]));
return MQ;
}
MathQuill.getLatexMathParser = function () {
return latexMathParser;
};
MathQuill.noConflict = function() {
window.MathQuill = origMathQuill;
return MathQuill;
};
var origMathQuill = window.MathQuill;
window.MathQuill = MathQuill;
function RootBlockMixin(_) {
var names = 'moveOutOf deleteOutOf selectOutOf upOutOf downOutOf'.split(' ');
for (var i = 0; i < names.length; i += 1) (function(name) {
_[name] = function(dir) { this.controller.handle(name, dir); };
}(names[i]));
_.reflow = function() {
this.controller.handle('reflow');
this.controller.handle('edited');
this.controller.handle('edit');
};
}
/*************************************************
* Sane Keyboard Events Shim
*
* An abstraction layer wrapping the textarea in
* an object with methods to manipulate and listen
* to events on, that hides all the nasty cross-
* browser incompatibilities behind a uniform API.
*
* Design goal: This is a *HARD* internal
* abstraction barrier. Cross-browser
* inconsistencies are not allowed to leak through
* and be dealt with by event handlers. All future
* cross-browser issues that arise must be dealt
* with here, and if necessary, the API updated.
*
* Organization:
* - key values map and stringify()
* - saneKeyboardEvents()
* + defer() and flush()
* + event handler logic
* + attach event handlers and export methods
************************************************/
var saneKeyboardEvents = (function() {
// The following [key values][1] map was compiled from the
// [DOM3 Events appendix section on key codes][2] and
// [a widely cited report on cross-browser tests of key codes][3],
// except for 10: 'Enter', which I've empirically observed in Safari on iOS
// and doesn't appear to conflict with any other known key codes.
//
// [1]: http://www.w3.org/TR/2012/WD-DOM-Level-3-Events-20120614/#keys-keyvalues
// [2]: http://www.w3.org/TR/2012/WD-DOM-Level-3-Events-20120614/#fixed-virtual-key-codes
// [3]: http://unixpapa.com/js/key.html
var KEY_VALUES = {
8: 'Backspace',
9: 'Tab',
10: 'Enter', // for Safari on iOS
13: 'Enter',
16: 'Shift',
17: 'Control',
18: 'Alt',
20: 'CapsLock',
27: 'Esc',
32: 'Spacebar',
33: 'PageUp',
34: 'PageDown',
35: 'End',
36: 'Home',
37: 'Left',
38: 'Up',
39: 'Right',
40: 'Down',
45: 'Insert',
46: 'Del',
144: 'NumLock'
};
// To the extent possible, create a normalized string representation
// of the key combo (i.e., key code and modifier keys).
function stringify(evt) {
var which = evt.which || evt.keyCode;
var keyVal = KEY_VALUES[which];
var key;
var modifiers = [];
if (evt.ctrlKey) modifiers.push('Ctrl');
if (evt.originalEvent && evt.originalEvent.metaKey) modifiers.push('Meta');
if (evt.altKey) modifiers.push('Alt');
if (evt.shiftKey) modifiers.push('Shift');
key = keyVal || String.fromCharCode(which);
if (!modifiers.length && !keyVal) return key;
modifiers.push(key);
return modifiers.join('-');
}
// create a keyboard events shim that calls callbacks at useful times
// and exports useful public methods
return function saneKeyboardEvents(el, handlers) {
var keydown = null;
var keypress = null;
var textarea = jQuery(el);
var target = jQuery(handlers.container || textarea);
// checkTextareaFor() is called after key or clipboard events to
// say "Hey, I think something was just typed" or "pasted" etc,
// so that at all subsequent opportune times (next event or timeout),
// will check for expected typed or pasted text.
// Need to check repeatedly because #135: in Safari 5.1 (at least),
// after selecting something and then typing, the textarea is
// incorrectly reported as selected during the input event (but not
// subsequently).
var checkTextarea = noop, timeoutId;
function checkTextareaFor(checker) {
checkTextarea = checker;
clearTimeout(timeoutId);
timeoutId = setTimeout(checker);
}
function checkTextareaOnce(checker) {
checkTextareaFor(function(e) {
checkTextarea = noop;
clearTimeout(timeoutId);
checker(e);
});
}
target.bind('keydown keypress input keyup focusout paste', function(e) { checkTextarea(e); });
// -*- public methods -*- //
function select(text) {
// check textarea at least once/one last time before munging (so
// no race condition if selection happens after keypress/paste but
// before checkTextarea), then never again ('cos it's been munged)
checkTextarea();
checkTextarea = noop;
clearTimeout(timeoutId);
textarea.val(text);
if (text && textarea[0].select) textarea[0].select();
shouldBeSelected = !!text;
}
var shouldBeSelected = false;
// -*- helper subroutines -*- //
// Determine whether there's a selection in the textarea.
// This will always return false in IE < 9, which don't support
// HTMLTextareaElement::selection{Start,End}.
function hasSelection() {
var dom = textarea[0];
if (!('selectionStart' in dom)) return false;
return dom.selectionStart !== dom.selectionEnd;
}
function handleKey() {
handlers.keystroke(stringify(keydown), keydown);
}
// -*- event handlers -*- //
function onKeydown(e) {
keydown = e;
keypress = null;
if (shouldBeSelected) checkTextareaOnce(function(e) {
if (!(e && e.type === 'focusout') && textarea[0].select) {
// re-select textarea in case it's an unrecognized key that clears
// the selection, then never again, 'cos next thing might be blur
textarea[0].select();
}
});
handleKey();
}
function onKeypress(e) {
// call the key handler for repeated keypresses.
// This excludes keypresses that happen directly
// after keydown. In that case, there will be
// no previous keypress, so we skip it here
if (keydown && keypress) handleKey();
keypress = e;
checkTextareaFor(typedText);
}
function onKeyup(e) {
// Handle case of no keypress event being sent
if (!!keydown && !keypress) checkTextareaFor(typedText);
}
function typedText() {
// If there is a selection, the contents of the textarea couldn't
// possibly have just been typed in.
// This happens in browsers like Firefox and Opera that fire
// keypress for keystrokes that are not text entry and leave the
// selection in the textarea alone, such as Ctrl-C.
// Note: we assume that browsers that don't support hasSelection()
// also never fire keypress on keystrokes that are not text entry.
// This seems reasonably safe because:
// - all modern browsers including IE 9+ support hasSelection(),
// making it extremely unlikely any browser besides IE < 9 won't
// - as far as we know IE < 9 never fires keypress on keystrokes
// that aren't text entry, which is only as reliable as our
// tests are comprehensive, but the IE < 9 way to do
// hasSelection() is poorly documented and is also only as
// reliable as our tests are comprehensive
// If anything like #40 or #71 is reported in IE < 9, see
// b1318e5349160b665003e36d4eedd64101ceacd8
if (hasSelection()) return;
var text = textarea.val();
if (text.length === 1) {
textarea.val('');
handlers.typedText(text);
} // in Firefox, keys that don't type text, just clear seln, fire keypress
// https://github.com/mathquill/mathquill/issues/293#issuecomment-40997668
else if (text && textarea[0].select) textarea[0].select(); // re-select if that's why we're here
}
function onBlur() { keydown = keypress = null; }
function onPaste(e) {
// browsers are dumb.
//
// In Linux, middle-click pasting causes onPaste to be called,
// when the textarea is not necessarily focused. We focus it
// here to ensure that the pasted text actually ends up in the
// textarea.
//
// It's pretty nifty that by changing focus in this handler,
// we can change the target of the default action. (This works
// on keydown too, FWIW).
//
// And by nifty, we mean dumb (but useful sometimes).
textarea.focus();
checkTextareaFor(pastedText);
}
function pastedText() {
var text = textarea.val();
textarea.val('');
if (text) handlers.paste(text);
}
// -*- attach event handlers -*- //
target.bind({
keydown: onKeydown,
keypress: onKeypress,
keyup: onKeyup,
focusout: onBlur,
cut: function() { checkTextareaOnce(function() { handlers.cut(); }); },
copy: function() { checkTextareaOnce(function() { handlers.copy(); }); },
paste: onPaste
});
// -*- export public methods -*- //
return {
select: select
};
};
}());
var Parser = P(function(_, super_, Parser) {
// The Parser object is a wrapper for a parser function.
// Externally, you use one to parse a string by calling
// var result = SomeParser.parse('Me Me Me! Parse Me!');
// You should never call the constructor, rather you should
// construct your Parser from the base parsers and the
// parser combinator methods.
function parseError(stream, message) {
if (stream) {
stream = "'"+stream+"'";
}
else {
stream = 'EOF';
}
throw 'Parse Error: '+message+' at '+stream;
}
_.init = function(body) { this._ = body; };
_.parse = function(stream) {
return this.skip(eof)._(''+stream, success, parseError);
function success(stream, result) { return result; }
};
// -*- primitive combinators -*- //
_.or = function(alternative) {
pray('or is passed a parser', alternative instanceof Parser);
var self = this;
return Parser(function(stream, onSuccess, onFailure) {
return self._(stream, onSuccess, failure);
function failure(newStream) {
return alternative._(stream, onSuccess, onFailure);
}
});
};
_.then = function(next) {
var self = this;
return Parser(function(stream, onSuccess, onFailure) {
return self._(stream, success, onFailure);
function success(newStream, result) {
var nextParser = (next instanceof Parser ? next : next(result));
pray('a parser is returned', nextParser instanceof Parser);
return nextParser._(newStream, onSuccess, onFailure);
}
});
};
// -*- optimized iterative combinators -*- //
_.many = function() {
var self = this;
return Parser(function(stream, onSuccess, onFailure) {
var xs = [];
while (self._(stream, success, failure));
return onSuccess(stream, xs);
function success(newStream, x) {
stream = newStream;
xs.push(x);
return true;
}
function failure() {
return false;
}
});
};
_.times = function(min, max) {
if (arguments.length < 2) max = min;
var self = this;
return Parser(function(stream, onSuccess, onFailure) {
var xs = [];
var result = true;
var failure;
for (var i = 0; i < min; i += 1) {
result = self._(stream, success, firstFailure);
if (!result) return onFailure(stream, failure);
}
for (; i < max && result; i += 1) {
result = self._(stream, success, secondFailure);
}
return onSuccess(stream, xs);
function success(newStream, x) {
xs.push(x);
stream = newStream;
return tr