| /** |
| * 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; |
| }; |
| _.empty = function() { |
| var root = this.__controller.root, cursor = this.__controller.cursor; |
| root.eachChild('postOrder', 'dispose'); |
| root.ends[L] = root.ends[R] = 0; |
| root.jQ.empty(); |
| delete cursor.selection; |
| cursor.insAtRightEnd(root); |
| 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.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 true; |
| } |
| |
| function firstFailure(newStream, msg) { |
| failure = msg; |
| stream = newStream; |
| return false; |
| } |
| |
| function secondFailure(newStream, msg) { |
| return false; |
| } |
| }); |
| }; |
| |
| // -*- higher-level combinators -*- // |
| _.result = function(res) { return this.then(succeed(res)); }; |
| _.atMost = function(n) { return this.times(0, n); }; |
| _.atLeast = function(n) { |
| var self = this; |
| return self.times(n).then(function(start) { |
| return self.many().map(function(end) { |
| return start.concat(end); |
| }); |
| }); |
| }; |
| |
| _.map = function(fn) { |
| return this.then(function(result) { return succeed(fn(result)); }); |
| }; |
| |
| _.skip = function(two) { |
| return this.then(function(result) { return two.result(result); }); |
| }; |
| |
| // -*- primitive parsers -*- // |
| var string = this.string = function(str) { |
| var len = str.length; |
| var expected = "expected '"+str+"'"; |
| |
| return Parser(function(stream, onSuccess, onFailure) { |
| var head = stream.slice(0, len); |
| |
| if (head === str) { |
| return onSuccess(stream.slice(len), head); |
| } |
| else { |
| return onFailure(stream, expected); |
| } |
| }); |
| }; |
| |
| var regex = this.regex = function(re) { |
| pray('regexp parser is anchored', re.toString().charAt(1) === '^'); |
| |
| var expected = 'expected '+re; |
| |
| return Parser(function(stream, onSuccess, onFailure) { |
| var match = re.exec(stream); |
| |
| if (match) { |
| var result = match[0]; |
| return onSuccess(stream.slice(result.length), result); |
| } |
| else { |
| return onFailure(stream, expected); |
| } |
| }); |
| }; |
| |
| var succeed = Parser.succeed = function(result) { |
| return Parser(function(stream, onSuccess) { |
| return onSuccess(stream, result); |
| }); |
| }; |
| |
| var fail = Parser.fail = function(msg) { |
| return Parser(function(stream, _, onFailure) { |
| return onFailure(stream, msg); |
| }); |
| }; |
| |
| var letter = Parser.letter = regex(/^[a-z]/i); |
| var letters = Parser.letters = regex(/^[a-z]*/i); |
| var digit = Parser.digit = regex(/^[0-9]/); |
| var digits = Parser.digits = regex(/^[0-9]*/); |
| var whitespace = Parser.whitespace = regex(/^\s+/); |
| var optWhitespace = Parser.optWhitespace = regex(/^\s*/); |
| |
| var any = Parser.any = Parser(function(stream, onSuccess, onFailure) { |
| if (!stream) return onFailure(stream, 'expected any character'); |
| |
| return onSuccess(stream.slice(1), stream.charAt(0)); |
| }); |
| |
| var all = Parser.all = Parser(function(stream, onSuccess, onFailure) { |
| return onSuccess('', stream); |
| }); |
| |
| var eof = Parser.eof = Parser(function(stream, onSuccess, onFailure) { |
| if (stream) return onFailure(stream, 'expected EOF'); |
| |
| return onSuccess(stream, stream); |
| }); |
| }); |
| // Parser MathBlock |
| var latexMathParser = (function() { |
| function commandToBlock(cmd) { // can also take in a Fragment |
| var block = MathBlock(); |
| cmd.adopt(block, 0, 0); |
| return block; |
| } |
| function joinBlocks(blocks) { |
| var firstBlock = blocks[0] || MathBlock(); |
| |
| for (var i = 1; i < blocks.length; i += 1) { |
| blocks[i].children().adopt(firstBlock, firstBlock.ends[R], 0); |
| } |
| |
| return firstBlock; |
| } |
| |
| var string = Parser.string; |
| var regex = Parser.regex; |
| var letter = Parser.letter; |
| var any = Parser.any; |
| var optWhitespace = Parser.optWhitespace; |
| var succeed = Parser.succeed; |
| var fail = Parser.fail; |
| |
| // Parsers yielding either MathCommands, or Fragments of MathCommands |
| // (either way, something that can be adopted by a MathBlock) |
| var variable = letter.map(function(c) { return Letter(c); }); |
| var symbol = regex(/^[^${}\\_^]/).map(function(c) { return VanillaSymbol(c); }); |
| |
| var controlSequence = |
| regex(/^[^\\a-eg-zA-Z]/) // hotfix #164; match MathBlock::write |
| .or(string('\\').then( |
| regex(/^[a-z]+/i) |
| .or(regex(/^\s+/).result(' ')) |
| .or(any) |
| )).then(function(ctrlSeq) { |
| var cmdKlass = LatexCmds[ctrlSeq]; |
| |
| if (cmdKlass) { |
| return cmdKlass(ctrlSeq).parser(); |
| } |
| else { |
| return fail('unknown command: \\'+ctrlSeq); |
| } |
| }) |
| ; |
| |
| var command = |
| controlSequence |
| .or(variable) |
| .or(symbol) |
| ; |
| |
| // Parsers yielding MathBlocks |
| var mathGroup = string('{').then(function() { return mathSequence; }).skip(string('}')); |
| var mathBlock = optWhitespace.then(mathGroup.or(command.map(commandToBlock))); |
| var mathSequence = mathBlock.many().map(joinBlocks).skip(optWhitespace); |
| |
| var optMathBlock = |
| string('[').then( |
| mathBlock.then(function(block) { |
| return block.join('latex') !== ']' ? succeed(block) : fail(); |
| }) |
| .many().map(joinBlocks).skip(optWhitespace) |
| ).skip(string(']')) |
| ; |
| |
| var latexMath = mathSequence; |
| |
| latexMath.block = mathBlock; |
| latexMath.optBlock = optMathBlock; |
| return latexMath; |
| })(); |
| |
| Controller.open(function(_, super_) { |
| _.exportLatex = function() { |
| return this.root.latex().replace(/(\\[a-z]+) (?![a-z])/ig,'$1'); |
| }; |
| _.writeLatex = function(latex) { |
| var cursor = this.notify('edit').cursor; |
| |
| var all = Parser.all; |
| var eof = Parser.eof; |
| |
| var block = latexMathParser.skip(eof).or(all.result(false)).parse(latex); |
| |
| if (block && !block.isEmpty()) { |
| block.children().adopt(cursor.parent, cursor[L], cursor[R]); |
| var jQ = block.jQize(); |
| jQ.insertBefore(cursor.jQ); |
| cursor[L] = block.ends[R]; |
| block.finalizeInsert(cursor.options, cursor); |
| if (block.ends[R][R].siblingCreated) block.ends[R][R].siblingCreated(cursor.options, L); |
| if (block.ends[L][L].siblingCreated) block.ends[L][L].siblingCreated(cursor.options, R); |
| cursor.parent.bubble('reflow'); |
| } |
| |
| return this; |
| }; |
| _.renderLatexMath = function(latex) { |
| var root = this.root, cursor = this.cursor; |
| |
| var all = Parser.all; |
| var eof = Parser.eof; |
| |
| var block = latexMathParser.skip(eof).or(all.result(false)).parse(latex); |
| |
| root.eachChild('postOrder', 'dispose'); |
| root.ends[L] = root.ends[R] = 0; |
| |
| if (block) { |
| block.children().adopt(root, 0, 0); |
| } |
| |
| var jQ = root.jQ; |
| |
| if (block) { |
| var html = block.join('html'); |
| jQ.html(html); |
| root.jQize(jQ.children()); |
| root.finalizeInsert(cursor.options); |
| } |
| else { |
| jQ.empty(); |
| } |
| |
| delete cursor.selection; |
| cursor.insAtRightEnd(root); |
| }; |
| _.renderLatexText = function(latex) { |
| var root = this.root, cursor = this.cursor; |
| |
| root.jQ.children().slice(1).remove(); |
| root.eachChild('postOrder', 'dispose'); |
| root.ends[L] = root.ends[R] = 0; |
| delete cursor.selection; |
| cursor.show().insAtRightEnd(root); |
| |
| var regex = Parser.regex; |
| var string = Parser.string; |
| var eof = Parser.eof; |
| var all = Parser.all; |
| |
| // Parser RootMathCommand |
| var mathMode = string('$').then(latexMathParser) |
| // because TeX is insane, math mode doesn't necessarily |
| // have to end. So we allow for the case that math mode |
| // continues to the end of the stream. |
| .skip(string('$').or(eof)) |
| .map(function(block) { |
| // HACK FIXME: this shouldn't have to have access to cursor |
| var rootMathCommand = RootMathCommand(cursor); |
| |
| rootMathCommand.createBlocks(); |
| var rootMathBlock = rootMathCommand.ends[L]; |
| block.children().adopt(rootMathBlock, 0, 0); |
| |
| return rootMathCommand; |
| }) |
| ; |
| |
| var escapedDollar = string('\\$').result('$'); |
| var textChar = escapedDollar.or(regex(/^[^$]/)).map(VanillaSymbol); |
| var latexText = mathMode.or(textChar).many(); |
| var commands = latexText.skip(eof).or(all.result(false)).parse(latex); |
| |
| if (commands) { |
| for (var i = 0; i < commands.length; i += 1) { |
| commands[i].adopt(root, root.ends[R], 0); |
| } |
| |
| root.jQize().appendTo(root.jQ); |
| |
| root.finalizeInsert(cursor.options); |
| } |
| }; |
| }); |
| Controller.open(function(_) { |
| _.focusBlurEvents = function() { |
| var ctrlr = this, root = ctrlr.root, cursor = ctrlr.cursor; |
| var blurTimeout; |
| ctrlr.textarea.focus(function() { |
| ctrlr.blurred = false; |
| clearTimeout(blurTimeout); |
| ctrlr.container.addClass('mq-focused'); |
| if (!cursor.parent) |
| cursor.insAtRightEnd(root); |
| if (cursor.selection) { |
| cursor.selection.jQ.removeClass('mq-blur'); |
| ctrlr.selectionChanged(); //re-select textarea contents after tabbing away and back |
| } |
| else |
| cursor.show(); |
| }).blur(function() { |
| ctrlr.blurred = true; |
| blurTimeout = setTimeout(function() { // wait for blur on window; if |
| root.postOrder('intentionalBlur'); // none, intentional blur: #264 |
| cursor.clearSelection().endSelection(); |
| blur(); |
| }); |
| $(window).bind('blur', windowBlur); |
| }); |
| function windowBlur() { // blur event also fired on window, just switching |
| clearTimeout(blurTimeout); // tabs/windows, not intentional blur |
| if (cursor.selection) cursor.selection.jQ.addClass('mq-blur'); |
| blur(); |
| } |
| function blur() { // not directly in the textarea blur handler so as to be |
| cursor.hide().parent.blur(); // synchronous with/in the same frame as |
| ctrlr.container.removeClass('mq-focused'); // clearing/blurring selection |
| $(window).unbind('blur', windowBlur); |
| } |
| ctrlr.blurred = true; |
| cursor.hide().parent.blur(); |
| }; |
| }); |
| |
| /** |
| * TODO: I wanted to move MathBlock::focus and blur here, it would clean |
| * up lots of stuff like, TextBlock::focus is set to MathBlock::focus |
| * and TextBlock::blur calls MathBlock::blur, when instead they could |
| * use inheritance and super_. |
| * |
| * Problem is, there's lots of calls to .focus()/.blur() on nodes |
| * outside Controller::focusBlurEvents(), such as .postOrder('blur') on |
| * insertion, which if MathBlock::blur becomes Node::blur, would add the |
| * 'blur' CSS class to all Symbol's (because .isEmpty() is true for all |
| * of them). |
| * |
| * I'm not even sure there aren't other troublesome calls to .focus() or |
| * .blur(), so this is TODO for now. |
| */ |
| /*********************************************** |
| * Export math in a human-readable text format |
| * As you can see, only half-baked so far. |
| **********************************************/ |
| |
| Controller.open(function(_, super_) { |
| _.exportText = function() { |
| return this.root.foldChildren('', function(text, child) { |
| return text + child.text(); |
| }); |
| }; |
| }); |
| /***************************************** |
| * Deals with the browser DOM events from |
| * interaction with the typist. |
| ****************************************/ |
| |
| Controller.open(function(_) { |
| _.keystroke = function(key, evt) { |
| this.cursor.parent.keystroke(key, evt, this); |
| }; |
| }); |
| |
| Node.open(function(_) { |
| _.keystroke = function(key, e, ctrlr) { |
| var cursor = ctrlr.cursor; |
| |
| switch (key) { |
| case 'Ctrl-Shift-Backspace': |
| case 'Ctrl-Backspace': |
| ctrlr.ctrlDeleteDir(L); |
| break; |
| |
| case 'Shift-Backspace': |
| case 'Backspace': |
| ctrlr.backspace(); |
| break; |
| |
| // Tab or Esc -> go one block right if it exists, else escape right. |
| case 'Esc': |
| case 'Tab': |
| ctrlr.escapeDir(R, key, e); |
| return; |
| |
| // Shift-Tab -> go one block left if it exists, else escape left. |
| case 'Shift-Tab': |
| case 'Shift-Esc': |
| ctrlr.escapeDir(L, key, e); |
| return; |
| |
| // End -> move to the end of the current block. |
| case 'End': |
| ctrlr.notify('move').cursor.insAtRightEnd(cursor.parent); |
| break; |
| |
| // Ctrl-End -> move all the way to the end of the root block. |
| case 'Ctrl-End': |
| ctrlr.notify('move').cursor.insAtRightEnd(ctrlr.root); |
| break; |
| |
| // Shift-End -> select to the end of the current block. |
| case 'Shift-End': |
| while (cursor[R]) { |
| ctrlr.selectRight(); |
| } |
| break; |
| |
| // Ctrl-Shift-End -> select to the end of the root block. |
| case 'Ctrl-Shift-End': |
| while (cursor[R] || cursor.parent !== ctrlr.root) { |
| ctrlr.selectRight(); |
| } |
| break; |
| |
| // Home -> move to the start of the root block or the current block. |
| case 'Home': |
| ctrlr.notify('move').cursor.insAtLeftEnd(cursor.parent); |
| break; |
| |
| // Ctrl-Home -> move to the start of the current block. |
| case 'Ctrl-Home': |
| ctrlr.notify('move').cursor.insAtLeftEnd(ctrlr.root); |
| break; |
| |
| // Shift-Home -> select to the start of the current block. |
| case 'Shift-Home': |
| while (cursor[L]) { |
| ctrlr.selectLeft(); |
| } |
| break; |
| |
| // Ctrl-Shift-Home -> move to the start of the root block. |
| case 'Ctrl-Shift-Home': |
| while (cursor[L] || cursor.parent !== ctrlr.root) { |
| ctrlr.selectLeft(); |
| } |
| break; |
| |
| case 'Left': ctrlr.moveLeft(); break; |
| case 'Shift-Left': ctrlr.selectLeft(); break; |
| case 'Ctrl-Left': break; |
| |
| case 'Right': ctrlr.moveRight(); break; |
| case 'Shift-Right': ctrlr.selectRight(); break; |
| case 'Ctrl-Right': break; |
| |
| case 'Up': ctrlr.moveUp(); break; |
| case 'Down': ctrlr.moveDown(); break; |
| |
| case 'Shift-Up': |
| if (cursor[L]) { |
| while (cursor[L]) ctrlr.selectLeft(); |
| } else { |
| ctrlr.selectLeft(); |
| } |
| |
| case 'Shift-Down': |
| if (cursor[R]) { |
| while (cursor[R]) ctrlr.selectRight(); |
| } |
| else { |
| ctrlr.selectRight(); |
| } |
| |
| case 'Ctrl-Up': break; |
| case 'Ctrl-Down': break; |
| |
| case 'Ctrl-Shift-Del': |
| case 'Ctrl-Del': |
| ctrlr.ctrlDeleteDir(R); |
| break; |
| |
| case 'Shift-Del': |
| case 'Del': |
| ctrlr.deleteForward(); |
| break; |
| |
| case 'Meta-A': |
| case 'Ctrl-A': |
| ctrlr.notify('move').cursor.insAtRightEnd(ctrlr.root); |
| while (cursor[L]) ctrlr.selectLeft(); |
| break; |
| |
| default: |
| return; |
| } |
| e.preventDefault(); |
| ctrlr.scrollHoriz(); |
| }; |
| |
| _.moveOutOf = // called by Controller::escapeDir, moveDir |
| _.moveTowards = // called by Controller::moveDir |
| _.deleteOutOf = // called by Controller::deleteDir |
| _.deleteTowards = // called by Controller::deleteDir |
| _.unselectInto = // called by Controller::selectDir |
| _.selectOutOf = // called by Controller::selectDir |
| _.selectTowards = // called by Controller::selectDir |
| function() { pray('overridden or never called on this node'); }; |
| }); |
| |
| Controller.open(function(_) { |
| this.onNotify(function(e) { |
| if (e === 'move' || e === 'upDown') this.show().clearSelection(); |
| }); |
| _.escapeDir = function(dir, key, e) { |
| prayDirection(dir); |
| var cursor = this.cursor; |
| |
| // only prevent default of Tab if not in the root editable |
| if (cursor.parent !== this.root) e.preventDefault(); |
| |
| // want to be a noop if in the root editable (in fact, Tab has an unrelated |
| // default browser action if so) |
| if (cursor.parent === this.root) return; |
| |
| cursor.parent.moveOutOf(dir, cursor); |
| return this.notify('move'); |
| }; |
| |
| optionProcessors.leftRightIntoCmdGoes = function(updown) { |
| if (updown && updown !== 'up' && updown !== 'down') { |
| throw '"up" or "down" required for leftRightIntoCmdGoes option, ' |
| + 'got "'+updown+'"'; |
| } |
| return updown; |
| }; |
| _.moveDir = function(dir) { |
| prayDirection(dir); |
| var cursor = this.cursor, updown = cursor.options.leftRightIntoCmdGoes; |
| |
| if (cursor.selection) { |
| cursor.insDirOf(dir, cursor.selection.ends[dir]); |
| } |
| else if (cursor[dir]) cursor[dir].moveTowards(dir, cursor, updown); |
| else cursor.parent.moveOutOf(dir, cursor, updown); |
| |
| return this.notify('move'); |
| }; |
| _.moveLeft = function() { return this.moveDir(L); }; |
| _.moveRight = function() { return this.moveDir(R); }; |
| |
| /** |
| * moveUp and moveDown have almost identical algorithms: |
| * - first check left and right, if so insAtLeft/RightEnd of them |
| * - else check the parent's 'upOutOf'/'downOutOf' property: |
| * + if it's a function, call it with the cursor as the sole argument and |
| * use the return value as if it were the value of the property |
| * + if it's a Node, jump up or down into it: |
| * - if there is a cached Point in the block, insert there |
| * - else, seekHoriz within the block to the current x-coordinate (to be |
| * as close to directly above/below the current position as possible) |
| * + unless it's exactly `true`, stop bubbling |
| */ |
| _.moveUp = function() { return moveUpDown(this, 'up'); }; |
| _.moveDown = function() { return moveUpDown(this, 'down'); }; |
| function moveUpDown(self, dir) { |
| var cursor = self.notify('upDown').cursor; |
| var dirInto = dir+'Into', dirOutOf = dir+'OutOf'; |
| if (cursor[R][dirInto]) cursor.insAtLeftEnd(cursor[R][dirInto]); |
| else if (cursor[L][dirInto]) cursor.insAtRightEnd(cursor[L][dirInto]); |
| else { |
| cursor.parent.bubble(function(ancestor) { |
| var prop = ancestor[dirOutOf]; |
| if (prop) { |
| if (typeof prop === 'function') prop = ancestor[dirOutOf](cursor); |
| if (prop instanceof Node) cursor.jumpUpDown(ancestor, prop); |
| if (prop !== true) return false; |
| } |
| }); |
| } |
| return self; |
| } |
| this.onNotify(function(e) { if (e !== 'upDown') this.upDownCache = {}; }); |
| |
| this.onNotify(function(e) { if (e === 'edit') this.show().deleteSelection(); }); |
| _.deleteDir = function(dir) { |
| prayDirection(dir); |
| var cursor = this.cursor; |
| |
| var hadSelection = cursor.selection; |
| this.notify('edit'); // deletes selection if present |
| if (!hadSelection) { |
| if (cursor[dir]) cursor[dir].deleteTowards(dir, cursor); |
| else cursor.parent.deleteOutOf(dir, cursor); |
| } |
| |
| if (cursor[L].siblingDeleted) cursor[L].siblingDeleted(cursor.options, R); |
| if (cursor[R].siblingDeleted) cursor[R].siblingDeleted(cursor.options, L); |
| cursor.parent.bubble('reflow'); |
| |
| return this; |
| }; |
| _.ctrlDeleteDir = function(dir) { |
| prayDirection(dir); |
| var cursor = this.cursor; |
| if (!cursor[dir] || cursor.selection) return this.deleteDir(dir); |
| |
| this.notify('edit'); |
| if (dir === L) { |
| Fragment(cursor.parent.ends[L], cursor[L]).remove(); |
| } else { |
| Fragment(cursor[R], cursor.parent.ends[R]).remove(); |
| }; |
| cursor.insAtDirEnd(dir, cursor.parent); |
| |
| if (cursor[L].siblingDeleted) cursor[L].siblingDeleted(cursor.options, R); |
| if (cursor[R].siblingDeleted) cursor[R].siblingDeleted(cursor.options, L); |
| cursor.parent.bubble('reflow'); |
| |
| return this; |
| }; |
| _.backspace = function() { return this.deleteDir(L); }; |
| _.deleteForward = function() { return this.deleteDir(R); }; |
| |
| this.onNotify(function(e) { if (e !== 'select') this.endSelection(); }); |
| _.selectDir = function(dir) { |
| var cursor = this.notify('select').cursor, seln = cursor.selection; |
| prayDirection(dir); |
| |
| if (!cursor.anticursor) cursor.startSelection(); |
| |
| var node = cursor[dir]; |
| if (node) { |
| // "if node we're selecting towards is inside selection (hence retracting) |
| // and is on the *far side* of the selection (hence is only node selected) |
| // and the anticursor is *inside* that node, not just on the other side" |
| if (seln && seln.ends[dir] === node && cursor.anticursor[-dir] !== node) { |
| node.unselectInto(dir, cursor); |
| } |
| else node.selectTowards(dir, cursor); |
| } |
| else cursor.parent.selectOutOf(dir, cursor); |
| |
| cursor.clearSelection(); |
| cursor.select() || cursor.show(); |
| }; |
| _.selectLeft = function() { return this.selectDir(L); }; |
| _.selectRight = function() { return this.selectDir(R); }; |
| }); |
| /********************************************* |
| * Manage the MathQuill instance's textarea |
| * (as owned by the Controller) |
| ********************************************/ |
| |
| Controller.open(function(_) { |
| Options.p.substituteTextarea = function() { |
| return $('<textarea autocapitalize=off autocomplete=off autocorrect=off ' + |
| 'spellcheck=false x-palm-disable-ste-all=true />')[0]; |
| }; |
| _.createTextarea = function() { |
| var textareaSpan = this.textareaSpan = $('<span class="mq-textarea"></span>'), |
| textarea = this.options.substituteTextarea(); |
| if (!textarea.nodeType) { |
| throw 'substituteTextarea() must return a DOM element, got ' + textarea; |
| } |
| textarea = this.textarea = $(textarea).appendTo(textareaSpan); |
| |
| var ctrlr = this; |
| ctrlr.cursor.selectionChanged = function() { ctrlr.selectionChanged(); }; |
| }; |
| _.selectionChanged = function() { |
| var ctrlr = this; |
| forceIERedraw(ctrlr.container[0]); |
| |
| // throttle calls to setTextareaSelection(), because setting textarea.value |
| // and/or calling textarea.select() can have anomalously bad performance: |
| // https://github.com/mathquill/mathquill/issues/43#issuecomment-1399080 |
| if (ctrlr.textareaSelectionTimeout === undefined) { |
| ctrlr.textareaSelectionTimeout = setTimeout(function() { |
| ctrlr.setTextareaSelection(); |
| }); |
| } |
| }; |
| _.setTextareaSelection = function() { |
| this.textareaSelectionTimeout = undefined; |
| var latex = ''; |
| if (this.cursor.selection) { |
| latex = this.cursor.selection.join('latex'); |
| if (this.options.statelessClipboard) { |
| // FIXME: like paste, only this works for math fields; should ask parent |
| latex = '$' + latex + '$'; |
| } |
| } |
| this.selectFn(latex); |
| }; |
| _.staticMathTextareaEvents = function() { |
| var ctrlr = this, root = ctrlr.root, cursor = ctrlr.cursor, |
| textarea = ctrlr.textarea, textareaSpan = ctrlr.textareaSpan; |
| |
| this.container.prepend(jQuery('<span class="mq-selectable">') |
| .text('$'+ctrlr.exportLatex()+'$')); |
| ctrlr.blurred = true; |
| textarea.bind('cut paste', false) |
| .bind('copy', function() { ctrlr.setTextareaSelection(); }) |
| .focus(function() { ctrlr.blurred = false; }).blur(function() { |
| if (cursor.selection) cursor.selection.clear(); |
| setTimeout(detach); //detaching during blur explodes in WebKit |
| }); |
| function detach() { |
| textareaSpan.detach(); |
| ctrlr.blurred = true; |
| } |
| |
| ctrlr.selectFn = function(text) { |
| textarea.val(text); |
| if (text) textarea.select(); |
| }; |
| }; |
| Options.p.substituteKeyboardEvents = saneKeyboardEvents; |
| _.editablesTextareaEvents = function() { |
| var ctrlr = this, textarea = ctrlr.textarea, textareaSpan = ctrlr.textareaSpan; |
| |
| var keyboardEventsShim = this.options.substituteKeyboardEvents(textarea, this); |
| this.selectFn = function(text) { keyboardEventsShim.select(text); }; |
| this.container.prepend(textareaSpan); |
| this.focusBlurEvents(); |
| }; |
| _.typedText = function(ch) { |
| if (ch === '\n') return this.handle('enter'); |
| var cursor = this.notify().cursor; |
| cursor.parent.write(cursor, ch); |
| this.scrollHoriz(); |
| }; |
| _.cut = function() { |
| var ctrlr = this, cursor = ctrlr.cursor; |
| if (cursor.selection) { |
| setTimeout(function() { |
| ctrlr.notify('edit'); // deletes selection if present |
| cursor.parent.bubble('reflow'); |
| }); |
| } |
| }; |
| _.copy = function() { |
| this.setTextareaSelection(); |
| }; |
| _.paste = function(text) { |
| // TODO: document `statelessClipboard` config option in README, after |
| // making it work like it should, that is, in both text and math mode |
| // (currently only works in math fields, so worse than pointless, it |
| // only gets in the way by \text{}-ifying pasted stuff and $-ifying |
| // cut/copied LaTeX) |
| if (this.options.statelessClipboard) { |
| if (text.slice(0,1) === '$' && text.slice(-1) === '$') { |
| text = text.slice(1, -1); |
| } |
| else { |
| text = '\\text{'+text+'}'; |
| } |
| } |
| // FIXME: this always inserts math or a TextBlock, even in a RootTextBlock |
| this.writeLatex(text).cursor.show(); |
| }; |
| }); |
| /******************************************************** |
| * Deals with mouse events for clicking, drag-to-select |
| *******************************************************/ |
| |
| Controller.open(function(_) { |
| Options.p.ignoreNextMousedown = noop; |
| _.delegateMouseEvents = function() { |
| var ultimateRootjQ = this.root.jQ; |
| //drag-to-select event handling |
| this.container.bind('mousedown.mathquill', function(e) { |
| var rootjQ = $(e.target).closest('.mq-root-block'); |
| var root = Node.byId[rootjQ.attr(mqBlockId) || ultimateRootjQ.attr(mqBlockId)]; |
| var ctrlr = root.controller, cursor = ctrlr.cursor, blink = cursor.blink; |
| var textareaSpan = ctrlr.textareaSpan, textarea = ctrlr.textarea; |
| |
| e.preventDefault(); // doesn't work in IE\u22648, but it's a one-line fix: |
| e.target.unselectable = true; // http://jsbin.com/yagekiji/1 |
| |
| if (cursor.options.ignoreNextMousedown(e)) return; |
| else cursor.options.ignoreNextMousedown = noop; |
| |
| var target; |
| function mousemove(e) { target = $(e.target); } |
| function docmousemove(e) { |
| if (!cursor.anticursor) cursor.startSelection(); |
| ctrlr.seek(target, e.pageX, e.pageY).cursor.select(); |
| target = undefined; |
| } |
| // outside rootjQ, the MathQuill node corresponding to the target (if any) |
| // won't be inside this root, so don't mislead Controller::seek with it |
| |
| function mouseup(e) { |
| cursor.blink = blink; |
| if (!cursor.selection) { |
| if (ctrlr.editable) { |
| cursor.show(); |
| } |
| else { |
| textareaSpan.detach(); |
| } |
| } |
| |
| // delete the mouse handlers now that we're not dragging anymore |
| rootjQ.unbind('mousemove', mousemove); |
| $(e.target.ownerDocument).unbind('mousemove', docmousemove).unbind('mouseup', mouseup); |
| } |
| |
| if (ctrlr.blurred) { |
| if (!ctrlr.editable) rootjQ.prepend(textareaSpan); |
| textarea.focus(); |
| } |
| |
| cursor.blink = noop; |
| ctrlr.seek($(e.target), e.pageX, e.pageY).cursor.startSelection(); |
| |
| rootjQ.mousemove(mousemove); |
| $(e.target.ownerDocument).mousemove(docmousemove).mouseup(mouseup); |
| // listen on document not just body to not only hear about mousemove and |
| // mouseup on page outside field, but even outside page, except iframes: https://github.com/mathquill/mathquill/commit/8c50028afcffcace655d8ae2049f6e02482346c5#commitcomment-6175800 |
| }); |
| } |
| }); |
| |
| Controller.open(function(_) { |
| _.seek = function(target, pageX, pageY) { |
| var cursor = this.notify('select').cursor; |
| |
| if (target) { |
| var nodeId = target.attr(mqBlockId) || target.attr(mqCmdId); |
| if (!nodeId) { |
| var targetParent = target.parent(); |
| nodeId = targetParent.attr(mqBlockId) || targetParent.attr(mqCmdId); |
| } |
| } |
| var node = nodeId ? Node.byId[nodeId] : this.root; |
| pray('nodeId is the id of some Node that exists', node); |
| |
| // don't clear selection until after getting node from target, in case |
| // target was selection span, otherwise target will have no parent and will |
| // seek from root, which is less accurate (e.g. fraction) |
| cursor.clearSelection().show(); |
| |
| node.seek(pageX, cursor); |
| this.scrollHoriz(); // before .selectFrom when mouse-selecting, so |
| // always hits no-selection case in scrollHoriz and scrolls slower |
| return this; |
| }; |
| }); |
| /*********************************************** |
| * Horizontal panning for editable fields that |
| * overflow their width |
| **********************************************/ |
| |
| Controller.open(function(_) { |
| _.scrollHoriz = function() { |
| var cursor = this.cursor, seln = cursor.selection; |
| var rootRect = this.root.jQ[0].getBoundingClientRect(); |
| if (!seln) { |
| var x = cursor.jQ[0].getBoundingClientRect().left; |
| if (x > rootRect.right - 20) var scrollBy = x - (rootRect.right - 20); |
| else if (x < rootRect.left + 20) var scrollBy = x - (rootRect.left + 20); |
| else return; |
| } |
| else { |
| var rect = seln.jQ[0].getBoundingClientRect(); |
| var overLeft = rect.left - (rootRect.left + 20); |
| var overRight = rect.right - (rootRect.right - 20); |
| if (seln.ends[L] === cursor[R]) { |
| if (overLeft < 0) var scrollBy = overLeft; |
| else if (overRight > 0) { |
| if (rect.left - overRight < rootRect.left + 20) var scrollBy = overLeft; |
| else var scrollBy = overRight; |
| } |
| else return; |
| } |
| else { |
| if (overRight > 0) var scrollBy = overRight; |
| else if (overLeft < 0) { |
| if (rect.right - overLeft > rootRect.right - 20) var scrollBy = overRight; |
| else var scrollBy = overLeft; |
| } |
| else return; |
| } |
| } |
| this.root.jQ.stop().animate({ scrollLeft: '+=' + scrollBy}, 100); |
| }; |
| }); |
| /************************************************* |
| * Abstract classes of math blocks and commands. |
| ************************************************/ |
| |
| /** |
| * Math tree node base class. |
| * Some math-tree-specific extensions to Node. |
| * Both MathBlock's and MathCommand's descend from it. |
| */ |
| var MathElement = P(Node, function(_, super_) { |
| _.finalizeInsert = function(options, cursor) { // `cursor` param is only for |
| // SupSub::contactWeld, and is deliberately only passed in by writeLatex, |
| // see ea7307eb4fac77c149a11ffdf9a831df85247693 |
| var self = this; |
| self.postOrder('finalizeTree', options); |
| self.postOrder('contactWeld', cursor); |
| |
| // note: this order is important. |
| // empty elements need the empty box provided by blur to |
| // be present in order for their dimensions to be measured |
| // correctly by 'reflow' handlers. |
| self.postOrder('blur'); |
| |
| self.postOrder('reflow'); |
| if (self[R].siblingCreated) self[R].siblingCreated(options, L); |
| if (self[L].siblingCreated) self[L].siblingCreated(options, R); |
| self.bubble('reflow'); |
| }; |
| }); |
| |
| /** |
| * Commands and operators, like subscripts, exponents, or fractions. |
| * Descendant commands are organized into blocks. |
| */ |
| var MathCommand = P(MathElement, function(_, super_) { |
| _.init = function(ctrlSeq, htmlTemplate, textTemplate) { |
| var cmd = this; |
| super_.init.call(cmd); |
| |
| if (!cmd.ctrlSeq) cmd.ctrlSeq = ctrlSeq; |
| if (htmlTemplate) cmd.htmlTemplate = htmlTemplate; |
| if (textTemplate) cmd.textTemplate = textTemplate; |
| }; |
| |
| // obvious methods |
| _.replaces = function(replacedFragment) { |
| replacedFragment.disown(); |
| this.replacedFragment = replacedFragment; |
| }; |
| _.isEmpty = function() { |
| return this.foldChildren(true, function(isEmpty, child) { |
| return isEmpty && child.isEmpty(); |
| }); |
| }; |
| |
| _.parser = function() { |
| var block = latexMathParser.block; |
| var self = this; |
| |
| return block.times(self.numBlocks()).map(function(blocks) { |
| self.blocks = blocks; |
| |
| for (var i = 0; i < blocks.length; i += 1) { |
| blocks[i].adopt(self, self.ends[R], 0); |
| } |
| |
| return self; |
| }); |
| }; |
| |
| // createLeftOf(cursor) and the methods it calls |
| _.createLeftOf = function(cursor) { |
| var cmd = this; |
| var replacedFragment = cmd.replacedFragment; |
| |
| cmd.createBlocks(); |
| super_.createLeftOf.call(cmd, cursor); |
| if (replacedFragment) { |
| replacedFragment.adopt(cmd.ends[L], 0, 0); |
| replacedFragment.jQ.appendTo(cmd.ends[L].jQ); |
| } |
| cmd.finalizeInsert(cursor.options); |
| cmd.placeCursor(cursor); |
| }; |
| _.createBlocks = function() { |
| var cmd = this, |
| numBlocks = cmd.numBlocks(), |
| blocks = cmd.blocks = Array(numBlocks); |
| |
| for (var i = 0; i < numBlocks; i += 1) { |
| var newBlock = blocks[i] = MathBlock(); |
| newBlock.adopt(cmd, cmd.ends[R], 0); |
| } |
| }; |
| _.placeCursor = function(cursor) { |
| //insert the cursor at the right end of the first empty child, searching |
| //left-to-right, or if none empty, the right end child |
| cursor.insAtRightEnd(this.foldChildren(this.ends[L], function(leftward, child) { |
| return leftward.isEmpty() ? leftward : child; |
| })); |
| }; |
| |
| // editability methods: called by the cursor for editing, cursor movements, |
| // and selection of the MathQuill tree, these all take in a direction and |
| // the cursor |
| _.moveTowards = function(dir, cursor, updown) { |
| var updownInto = updown && this[updown+'Into']; |
| cursor.insAtDirEnd(-dir, updownInto || this.ends[-dir]); |
| }; |
| _.deleteTowards = function(dir, cursor) { |
| if (this.isEmpty()) cursor[dir] = this.remove()[dir]; |
| else this.moveTowards(dir, cursor, null); |
| }; |
| _.selectTowards = function(dir, cursor) { |
| cursor[-dir] = this; |
| cursor[dir] = this[dir]; |
| }; |
| _.selectChildren = function() { |
| return Selection(this, this); |
| }; |
| _.unselectInto = function(dir, cursor) { |
| cursor.insAtDirEnd(-dir, cursor.anticursor.ancestors[this.id]); |
| }; |
| _.seek = function(pageX, cursor) { |
| function getBounds(node) { |
| var bounds = {} |
| bounds[L] = node.jQ.offset().left; |
| bounds[R] = bounds[L] + node.jQ.outerWidth(); |
| return bounds; |
| } |
| |
| var cmd = this; |
| var cmdBounds = getBounds(cmd); |
| |
| if (pageX < cmdBounds[L]) return cursor.insLeftOf(cmd); |
| if (pageX > cmdBounds[R]) return cursor.insRightOf(cmd); |
| |
| var leftLeftBound = cmdBounds[L]; |
| cmd.eachChild(function(block) { |
| var blockBounds = getBounds(block); |
| if (pageX < blockBounds[L]) { |
| // closer to this block's left bound, or the bound left of that? |
| if (pageX - leftLeftBound < blockBounds[L] - pageX) { |
| if (block[L]) cursor.insAtRightEnd(block[L]); |
| else cursor.insLeftOf(cmd); |
| } |
| else cursor.insAtLeftEnd(block); |
| return false; |
| } |
| else if (pageX > blockBounds[R]) { |
| if (block[R]) leftLeftBound = blockBounds[R]; // continue to next block |
| else { // last (rightmost) block |
| // closer to this block's right bound, or the cmd's right bound? |
| if (cmdBounds[R] - pageX < pageX - blockBounds[R]) { |
| cursor.insRightOf(cmd); |
| } |
| else cursor.insAtRightEnd(block); |
| } |
| } |
| else { |
| block.seek(pageX, cursor); |
| return false; |
| } |
| }); |
| } |
| |
| // methods involved in creating and cross-linking with HTML DOM nodes |
| /* |
| They all expect an .htmlTemplate like |
| '<span>&0</span>' |
| or |
| '<span><span>&0</span><span>&1</span></span>' |
| |
| See html.test.js for more examples. |
| |
| Requirements: |
| - For each block of the command, there must be exactly one "block content |
| marker" of the form '&<number>' where <number> is the 0-based index of the |
| block. (Like the LaTeX \newcommand syntax, but with a 0-based rather than |
| 1-based index, because JavaScript because C because Dijkstra.) |
| - The block content marker must be the sole contents of the containing |
| element, there can't even be surrounding whitespace, or else we can't |
| guarantee sticking to within the bounds of the block content marker when |
| mucking with the HTML DOM. |
| - The HTML not only must be well-formed HTML (of course), but also must |
| conform to the XHTML requirements on tags, specifically all tags must |
| either be self-closing (like '<br/>') or come in matching pairs. |
| Close tags are never optional. |
| |
| Note that &<number> isn't well-formed HTML; if you wanted a literal '&123', |
| your HTML template would have to have '&123'. |
| */ |
| _.numBlocks = function() { |
| var matches = this.htmlTemplate.match(/&\d+/g); |
| return matches ? matches.length : 0; |
| }; |
| _.html = function() { |
| // Render the entire math subtree rooted at this command, as HTML. |
| // Expects .createBlocks() to have been called already, since it uses the |
| // .blocks array of child blocks. |
| // |
| // See html.test.js for example templates and intended outputs. |
| // |
| // Given an .htmlTemplate as described above, |
| // - insert the mathquill-command-id attribute into all top-level tags, |
| // which will be used to set this.jQ in .jQize(). |
| // This is straightforward: |
| // * tokenize into tags and non-tags |
| // * loop through top-level tokens: |
| // * add #cmdId attribute macro to top-level self-closing tags |
| // * else add #cmdId attribute macro to top-level open tags |
| // * skip the matching top-level close tag and all tag pairs |
| // in between |
| // - for each block content marker, |
| // + replace it with the contents of the corresponding block, |
| // rendered as HTML |
| // + insert the mathquill-block-id attribute into the containing tag |
| // This is even easier, a quick regex replace, since block tags cannot |
| // contain anything besides the block content marker. |
| // |
| // Two notes: |
| // - The outermost loop through top-level tokens should never encounter any |
| // top-level close tags, because we should have first encountered a |
| // matching top-level open tag, all inner tags should have appeared in |
| // matching pairs and been skipped, and then we should have skipped the |
| // close tag in question. |
| // - All open tags should have matching close tags, which means our inner |
| // loop should always encounter a close tag and drop nesting to 0. If |
| // a close tag is missing, the loop will continue until i >= tokens.length |
| // and token becomes undefined. This will not infinite loop, even in |
| // production without pray(), because it will then TypeError on .slice(). |
| |
| var cmd = this; |
| var blocks = cmd.blocks; |
| var cmdId = ' mathquill-command-id=' + cmd.id; |
| var tokens = cmd.htmlTemplate.match(/<[^<>]+>|[^<>]+/g); |
| |
| pray('no unmatched angle brackets', tokens.join('') === this.htmlTemplate); |
| |
| // add cmdId to all top-level tags |
| for (var i = 0, token = tokens[0]; token; i += 1, token = tokens[i]) { |
| // top-level self-closing tags |
| if (token.slice(-2) === '/>') { |
| tokens[i] = token.slice(0,-2) + cmdId + '/>'; |
| } |
| // top-level open tags |
| else if (token.charAt(0) === '<') { |
| pray('not an unmatched top-level close tag', token.charAt(1) !== '/'); |
| |
| tokens[i] = token.slice(0,-1) + cmdId + '>'; |
| |
| // skip matching top-level close tag and all tag pairs in between |
| var nesting = 1; |
| do { |
| i += 1, token = tokens[i]; |
| pray('no missing close tags', token); |
| // close tags |
| if (token.slice(0,2) === '</') { |
| nesting -= 1; |
| } |
| // non-self-closing open tags |
| else if (token.charAt(0) === '<' && token.slice(-2) !== '/>') { |
| nesting += 1; |
| } |
| } while (nesting > 0); |
| } |
| } |
| return tokens.join('').replace(/>&(\d+)/g, function($0, $1) { |
| return ' mathquill-block-id=' + blocks[$1].id + '>' + blocks[$1].join('html'); |
| }); |
| }; |
| |
| // methods to export a string representation of the math tree |
| _.latex = function() { |
| return this.foldChildren(this.ctrlSeq, function(latex, child) { |
| return latex + '{' + (child.latex() || ' ') + '}'; |
| }); |
| }; |
| _.textTemplate = ['']; |
| _.text = function() { |
| var cmd = this, i = 0; |
| return cmd.foldChildren(cmd.textTemplate[i], function(text, child) { |
| i += 1; |
| var child_text = child.text(); |
| if (text && cmd.textTemplate[i] === '(' |
| && child_text[0] === '(' && child_text.slice(-1) === ')') |
| return text + child_text.slice(1, -1) + cmd.textTemplate[i]; |
| return text + child.text() + (cmd.textTemplate[i] || ''); |
| }); |
| }; |
| }); |
| |
| /** |
| * Lightweight command without blocks or children. |
| */ |
| var Symbol = P(MathCommand, function(_, super_) { |
| _.init = function(ctrlSeq, html, text) { |
| if (!text) text = ctrlSeq && ctrlSeq.length > 1 ? ctrlSeq.slice(1) : ctrlSeq; |
| |
| super_.init.call(this, ctrlSeq, html, [ text ]); |
| }; |
| |
| _.parser = function() { return Parser.succeed(this); }; |
| _.numBlocks = function() { return 0; }; |
| |
| _.replaces = function(replacedFragment) { |
| replacedFragment.remove(); |
| }; |
| _.createBlocks = noop; |
| |
| _.moveTowards = function(dir, cursor) { |
| cursor.jQ.insDirOf(dir, this.jQ); |
| cursor[-dir] = this; |
| cursor[dir] = this[dir]; |
| }; |
| _.deleteTowards = function(dir, cursor) { |
| cursor[dir] = this.remove()[dir]; |
| }; |
| _.seek = function(pageX, cursor) { |
| // insert at whichever side the click was closer to |
| if (pageX - this.jQ.offset().left < this.jQ.outerWidth()/2) |
| cursor.insLeftOf(this); |
| else |
| cursor.insRightOf(this); |
| }; |
| |
| _.latex = function(){ return this.ctrlSeq; }; |
| _.text = function(){ return this.textTemplate; }; |
| _.placeCursor = noop; |
| _.isEmpty = function(){ return true; }; |
| }); |
| var VanillaSymbol = P(Symbol, function(_, super_) { |
| _.init = function(ch, html) { |
| super_.init.call(this, ch, '<span>'+(html || ch)+'</span>'); |
| }; |
| }); |
| var BinaryOperator = P(Symbol, function(_, super_) { |
| _.init = function(ctrlSeq, html, text) { |
| super_.init.call(this, |
| ctrlSeq, '<span class="mq-binary-operator">'+html+'</span>', text |
| ); |
| }; |
| }); |
| |
| /** |
| * Children and parent of MathCommand's. Basically partitions all the |
| * symbols and operators that descend (in the Math DOM tree) from |
| * ancestor operators. |
| */ |
| var MathBlock = P(MathElement, function(_, super_) { |
| _.join = function(methodName) { |
| return this.foldChildren('', function(fold, child) { |
| return fold + child[methodName](); |
| }); |
| }; |
| _.html = function() { return this.join('html'); }; |
| _.latex = function() { return this.join('latex'); }; |
| _.text = function() { |
| return (this.ends[L] === this.ends[R] && this.ends[L] !== 0) ? |
| this.ends[L].text() : |
| this.join('text') |
| ; |
| }; |
| |
| _.keystroke = function(key, e, ctrlr) { |
| if (ctrlr.options.spaceBehavesLikeTab |
| && (key === 'Spacebar' || key === 'Shift-Spacebar')) { |
| e.preventDefault(); |
| ctrlr.escapeDir(key === 'Shift-Spacebar' ? L : R, key, e); |
| return; |
| } |
| return super_.keystroke.apply(this, arguments); |
| }; |
| |
| // editability methods: called by the cursor for editing, cursor movements, |
| // and selection of the MathQuill tree, these all take in a direction and |
| // the cursor |
| _.moveOutOf = function(dir, cursor, updown) { |
| var updownInto = updown && this.parent[updown+'Into']; |
| if (!updownInto && this[dir]) cursor.insAtDirEnd(-dir, this[dir]); |
| else cursor.insDirOf(dir, this.parent); |
| }; |
| _.selectOutOf = function(dir, cursor) { |
| cursor.insDirOf(dir, this.parent); |
| }; |
| _.deleteOutOf = function(dir, cursor) { |
| cursor.unwrapGramp(); |
| }; |
| _.seek = function(pageX, cursor) { |
| var node = this.ends[R]; |
| if (!node || node.jQ.offset().left + node.jQ.outerWidth() < pageX) { |
| return cursor.insAtRightEnd(this); |
| } |
| if (pageX < this.ends[L].jQ.offset().left) return cursor.insAtLeftEnd(this); |
| while (pageX < node.jQ.offset().left) node = node[L]; |
| return node.seek(pageX, cursor); |
| }; |
| _.chToCmd = function(ch, options) { |
| var cons; |
| // exclude f because it gets a dedicated command with more spacing |
| if (ch.match(/^[a-eg-zA-Z]$/)) |
| return Letter(ch); |
| else if (/^\d$/.test(ch)) |
| return Digit(ch); |
| else if (options && options.typingSlashWritesDivisionSymbol && ch === '/') |
| return LatexCmds['\u00f7'](ch); |
| else if (options && options.typingAsteriskWritesTimesSymbol && ch === '*') |
| return LatexCmds['\u00d7'](ch); |
| else if (cons = CharCmds[ch] || LatexCmds[ch]) |
| return cons(ch); |
| else |
| return VanillaSymbol(ch); |
| }; |
| _.write = function(cursor, ch) { |
| var cmd = this.chToCmd(ch, cursor.options); |
| if (cursor.selection) cmd.replaces(cursor.replaceSelection()); |
| cmd.createLeftOf(cursor.show()); |
| }; |
| |
| _.focus = function() { |
| this.jQ.addClass('mq-hasCursor'); |
| this.jQ.removeClass('mq-empty'); |
| |
| return this; |
| }; |
| _.blur = function() { |
| this.jQ.removeClass('mq-hasCursor'); |
| if (this.isEmpty()) |
| this.jQ.addClass('mq-empty'); |
| |
| return this; |
| }; |
| }); |
| |
| API.StaticMath = function(APIClasses) { |
| return P(APIClasses.AbstractMathQuill, function(_, super_) { |
| this.RootBlock = MathBlock; |
| _.__mathquillify = function(opts, interfaceVersion) { |
| this.config(opts); |
| super_.__mathquillify.call(this, 'mq-math-mode'); |
| this.__controller.delegateMouseEvents(); |
| this.__controller.staticMathTextareaEvents(); |
| return this; |
| }; |
| _.init = function() { |
| super_.init.apply(this, arguments); |
| this.__controller.root.postOrder( |
| 'registerInnerField', this.innerFields = [], APIClasses.MathField); |
| }; |
| _.latex = function() { |
| var returned = super_.latex.apply(this, arguments); |
| if (arguments.length > 0) { |
| this.__controller.root.postOrder( |
| 'registerInnerField', this.innerFields = [], APIClasses.MathField); |
| } |
| return returned; |
| }; |
| }); |
| }; |
| |
| var RootMathBlock = P(MathBlock, RootBlockMixin); |
| API.MathField = function(APIClasses) { |
| return P(APIClasses.EditableField, function(_, super_) { |
| this.RootBlock = RootMathBlock; |
| _.__mathquillify = function(opts, interfaceVersion) { |
| this.config(opts); |
| if (interfaceVersion > 1) this.__controller.root.reflow = noop; |
| super_.__mathquillify.call(this, 'mq-editable-field mq-math-mode'); |
| delete this.__controller.root.reflow; |
| return this; |
| }; |
| }); |
| }; |
| /************************************************* |
| * Abstract classes of text blocks |
| ************************************************/ |
| |
| /** |
| * Blocks of plain text, with one or two TextPiece's as children. |
| * Represents flat strings of typically serif-font Roman characters, as |
| * opposed to hierchical, nested, tree-structured math. |
| * Wraps a single HTMLSpanElement. |
| */ |
| var TextBlock = P(Node, function(_, super_) { |
| _.ctrlSeq = '\\text'; |
| |
| _.replaces = function(replacedText) { |
| if (replacedText instanceof Fragment) |
| this.replacedText = replacedText.remove().jQ.text(); |
| else if (typeof replacedText === 'string') |
| this.replacedText = replacedText; |
| }; |
| |
| _.jQadd = function(jQ) { |
| super_.jQadd.call(this, jQ); |
| if (this.ends[L]) this.ends[L].jQadd(this.jQ[0].firstChild); |
| }; |
| |
| _.createLeftOf = function(cursor) { |
| var textBlock = this; |
| super_.createLeftOf.call(this, cursor); |
| |
| if (textBlock[R].siblingCreated) textBlock[R].siblingCreated(cursor.options, L); |
| if (textBlock[L].siblingCreated) textBlock[L].siblingCreated(cursor.options, R); |
| textBlock.bubble('reflow'); |
| |
| cursor.insAtRightEnd(textBlock); |
| |
| if (textBlock.replacedText) |
| for (var i = 0; i < textBlock.replacedText.length; i += 1) |
| textBlock.write(cursor, textBlock.replacedText.charAt(i)); |
| }; |
| |
| _.parser = function() { |
| var textBlock = this; |
| |
| // TODO: correctly parse text mode |
| var string = Parser.string; |
| var regex = Parser.regex; |
| var optWhitespace = Parser.optWhitespace; |
| return optWhitespace |
| .then(string('{')).then(regex(/^[^}]*/)).skip(string('}')) |
| .map(function(text) { |
| if (text.length === 0) return Fragment(); |
| |
| TextPiece(text).adopt(textBlock, 0, 0); |
| return textBlock; |
| }) |
| ; |
| }; |
| |
| _.textContents = function() { |
| return this.foldChildren('', function(text, child) { |
| return text + child.text; |
| }); |
| }; |
| _.text = function() { return '"' + this.textContents() + '"'; }; |
| _.latex = function() { |
| var contents = this.textContents(); |
| if (contents.length === 0) return ''; |
| return '\\text{' + contents.replace(/\\/g, '\\backslash ').replace(/[{}]/g, '\\$&') + '}'; |
| }; |
| _.html = function() { |
| return ( |
| '<span class="mq-text-mode" mathquill-command-id='+this.id+'>' |
| + this.textContents() |
| + '</span>' |
| ); |
| }; |
| |
| // editability methods: called by the cursor for editing, cursor movements, |
| // and selection of the MathQuill tree, these all take in a direction and |
| // the cursor |
| _.moveTowards = function(dir, cursor) { cursor.insAtDirEnd(-dir, this); }; |
| _.moveOutOf = function(dir, cursor) { cursor.insDirOf(dir, this); }; |
| _.unselectInto = _.moveTowards; |
| |
| // TODO: make these methods part of a shared mixin or something. |
| _.selectTowards = MathCommand.prototype.selectTowards; |
| _.deleteTowards = MathCommand.prototype.deleteTowards; |
| |
| _.selectOutOf = function(dir, cursor) { |
| cursor.insDirOf(dir, this); |
| }; |
| _.deleteOutOf = function(dir, cursor) { |
| // backspace and delete at ends of block don't unwrap |
| if (this.isEmpty()) cursor.insRightOf(this); |
| }; |
| _.write = function(cursor, ch) { |
| cursor.show().deleteSelection(); |
| |
| if (ch !== '$') { |
| if (!cursor[L]) TextPiece(ch).createLeftOf(cursor); |
| else cursor[L].appendText(ch); |
| } |
| else if (this.isEmpty()) { |
| cursor.insRightOf(this); |
| VanillaSymbol('\\$','$').createLeftOf(cursor); |
| } |
| else if (!cursor[R]) cursor.insRightOf(this); |
| else if (!cursor[L]) cursor.insLeftOf(this); |
| else { // split apart |
| var leftBlock = TextBlock(); |
| var leftPc = this.ends[L]; |
| leftPc.disown().jQ.detach(); |
| leftPc.adopt(leftBlock, 0, 0); |
| |
| cursor.insLeftOf(this); |
| super_.createLeftOf.call(leftBlock, cursor); |
| } |
| }; |
| |
| _.seek = function(pageX, cursor) { |
| cursor.hide(); |
| var textPc = fuseChildren(this); |
| |
| // insert cursor at approx position in DOMTextNode |
| var avgChWidth = this.jQ.width()/this.text.length; |
| var approxPosition = Math.round((pageX - this.jQ.offset().left)/avgChWidth); |
| if (approxPosition <= 0) cursor.insAtLeftEnd(this); |
| else if (approxPosition >= textPc.text.length) cursor.insAtRightEnd(this); |
| else cursor.insLeftOf(textPc.splitRight(approxPosition)); |
| |
| // move towards mousedown (pageX) |
| var displ = pageX - cursor.show().offset().left; // displacement |
| var dir = displ && displ < 0 ? L : R; |
| var prevDispl = dir; |
| // displ * prevDispl > 0 iff displacement direction === previous direction |
| while (cursor[dir] && displ * prevDispl > 0) { |
| cursor[dir].moveTowards(dir, cursor); |
| prevDispl = displ; |
| displ = pageX - cursor.offset().left; |
| } |
| if (dir*displ < -dir*prevDispl) cursor[-dir].moveTowards(-dir, cursor); |
| |
| if (!cursor.anticursor) { |
| // about to start mouse-selecting, the anticursor is gonna get put here |
| this.anticursorPosition = cursor[L] && cursor[L].text.length; |
| // ^ get it? 'cos if there's no cursor[L], it's 0... I'm a terrible person. |
| } |
| else if (cursor.anticursor.parent === this) { |
| // mouse-selecting within this TextBlock, re-insert the anticursor |
| var cursorPosition = cursor[L] && cursor[L].text.length;; |
| if (this.anticursorPosition === cursorPosition) { |
| cursor.anticursor = Point.copy(cursor); |
| } |
| else { |
| if (this.anticursorPosition < cursorPosition) { |
| var newTextPc = cursor[L].splitRight(this.anticursorPosition); |
| cursor[L] = newTextPc; |
| } |
| else { |
| var newTextPc = cursor[R].splitRight(this.anticursorPosition - cursorPosition); |
| } |
| cursor.anticursor = Point(this, newTextPc[L], newTextPc); |
| } |
| } |
| }; |
| |
| _.blur = function(cursor) { |
| MathBlock.prototype.blur.call(this); |
| if (!cursor) return; |
| if (this.textContents() === '') { |
| this.remove(); |
| if (cursor[L] === this) cursor[L] = this[L]; |
| else if (cursor[R] === this) cursor[R] = this[R]; |
| } |
| else fuseChildren(this); |
| }; |
| |
| function fuseChildren(self) { |
| self.jQ[0].normalize(); |
| |
| var textPcDom = self.jQ[0].firstChild; |
| if (!textPcDom) return; |
| pray('only node in TextBlock span is Text node', textPcDom.nodeType === 3); |
| // nodeType === 3 has meant a Text node since ancient times: |
| // http://reference.sitepoint.com/javascript/Node/nodeType |
| |
| var textPc = TextPiece(textPcDom.data); |
| textPc.jQadd(textPcDom); |
| |
| self.children().disown(); |
| return textPc.adopt(self, 0, 0); |
| } |
| |
| _.focus = MathBlock.prototype.focus; |
| }); |
| |
| /** |
| * Piece of plain text, with a TextBlock as a parent and no children. |
| * Wraps a single DOMTextNode. |
| * For convenience, has a .text property that's just a JavaScript string |
| * mirroring the text contents of the DOMTextNode. |
| * Text contents must always be nonempty. |
| */ |
| var TextPiece = P(Node, function(_, super_) { |
| _.init = function(text) { |
| super_.init.call(this); |
| this.text = text; |
| }; |
| _.jQadd = function(dom) { this.dom = dom; this.jQ = $(dom); }; |
| _.jQize = function() { |
| return this.jQadd(document.createTextNode(this.text)); |
| }; |
| _.appendText = function(text) { |
| this.text += text; |
| this.dom.appendData(text); |
| }; |
| _.prependText = function(text) { |
| this.text = text + this.text; |
| this.dom.insertData(0, text); |
| }; |
| _.insTextAtDirEnd = function(text, dir) { |
| prayDirection(dir); |
| if (dir === R) this.appendText(text); |
| else this.prependText(text); |
| }; |
| _.splitRight = function(i) { |
| var newPc = TextPiece(this.text.slice(i)).adopt(this.parent, this, this[R]); |
| newPc.jQadd(this.dom.splitText(i)); |
| this.text = this.text.slice(0, i); |
| return newPc; |
| }; |
| |
| function endChar(dir, text) { |
| return text.charAt(dir === L ? 0 : -1 + text.length); |
| } |
| |
| _.moveTowards = function(dir, cursor) { |
| prayDirection(dir); |
| |
| var ch = endChar(-dir, this.text) |
| |
| var from = this[-dir]; |
| if (from) from.insTextAtDirEnd(ch, dir); |
| else TextPiece(ch).createDir(-dir, cursor); |
| |
| return this.deleteTowards(dir, cursor); |
| }; |
| |
| _.latex = function() { return this.text; }; |
| |
| _.deleteTowards = function(dir, cursor) { |
| if (this.text.length > 1) { |
| if (dir === R) { |
| this.dom.deleteData(0, 1); |
| this.text = this.text.slice(1); |
| } |
| else { |
| // note that the order of these 2 lines is annoyingly important |
| // (the second line mutates this.text.length) |
| this.dom.deleteData(-1 + this.text.length, 1); |
| this.text = this.text.slice(0, -1); |
| } |
| } |
| else { |
| this.remove(); |
| this.jQ.remove(); |
| cursor[dir] = this[dir]; |
| } |
| }; |
| |
| _.selectTowards = function(dir, cursor) { |
| prayDirection(dir); |
| var anticursor = cursor.anticursor; |
| |
| var ch = endChar(-dir, this.text) |
| |
| if (anticursor[dir] === this) { |
| var newPc = TextPiece(ch).createDir(dir, cursor); |
| anticursor[dir] = newPc; |
| cursor.insDirOf(dir, newPc); |
| } |
| else { |
| var from = this[-dir]; |
| if (from) from.insTextAtDirEnd(ch, dir); |
| else { |
| var newPc = TextPiece(ch).createDir(-dir, cursor); |
| newPc.jQ.insDirOf(-dir, cursor.selection.jQ); |
| } |
| |
| if (this.text.length === 1 && anticursor[-dir] === this) { |
| anticursor[-dir] = this[-dir]; // `this` will be removed in deleteTowards |
| } |
| } |
| |
| return this.deleteTowards(dir, cursor); |
| }; |
| }); |
| |
| LatexCmds.text = |
| LatexCmds.textnormal = |
| LatexCmds.textrm = |
| LatexCmds.textup = |
| LatexCmds.textmd = TextBlock; |
| |
| function makeTextBlock(latex, tagName, attrs) { |
| return P(TextBlock, { |
| ctrlSeq: latex, |
| htmlTemplate: '<'+tagName+' '+attrs+'>&0</'+tagName+'>' |
| }); |
| } |
| |
| LatexCmds.em = LatexCmds.italic = LatexCmds.italics = |
| LatexCmds.emph = LatexCmds.textit = LatexCmds.textsl = |
| makeTextBlock('\\textit', 'i', 'class="mq-text-mode"'); |
| LatexCmds.strong = LatexCmds.bold = LatexCmds.textbf = |
| makeTextBlock('\\textbf', 'b', 'class="mq-text-mode"'); |
| LatexCmds.sf = LatexCmds.textsf = |
| makeTextBlock('\\textsf', 'span', 'class="mq-sans-serif mq-text-mode"'); |
| LatexCmds.tt = LatexCmds.texttt = |
| makeTextBlock('\\texttt', 'span', 'class="mq-monospace mq-text-mode"'); |
| LatexCmds.textsc = |
| makeTextBlock('\\textsc', 'span', 'style="font-variant:small-caps" class="mq-text-mode"'); |
| LatexCmds.uppercase = |
| makeTextBlock('\\uppercase', 'span', 'style="text-transform:uppercase" class="mq-text-mode"'); |
| LatexCmds.lowercase = |
| makeTextBlock('\\lowercase', 'span', 'style="text-transform:lowercase" class="mq-text-mode"'); |
| |
| |
| var RootMathCommand = P(MathCommand, function(_, super_) { |
| _.init = function(cursor) { |
| super_.init.call(this, '$'); |
| this.cursor = cursor; |
| }; |
| _.htmlTemplate = '<span class="mq-math-mode">&0</span>'; |
| _.createBlocks = function() { |
| super_.createBlocks.call(this); |
| |
| this.ends[L].cursor = this.cursor; |
| this.ends[L].write = function(cursor, ch) { |
| if (ch !== '$') |
| MathBlock.prototype.write.call(this, cursor, ch); |
| else if (this.isEmpty()) { |
| cursor.insRightOf(this.parent); |
| this.parent.deleteTowards(dir, cursor); |
| VanillaSymbol('\\$','$').createLeftOf(cursor.show()); |
| } |
| else if (!cursor[R]) |
| cursor.insRightOf(this.parent); |
| else if (!cursor[L]) |
| cursor.insLeftOf(this.parent); |
| else |
| MathBlock.prototype.write.call(this, cursor, ch); |
| }; |
| }; |
| _.latex = function() { |
| return '$' + this.ends[L].latex() + '$'; |
| }; |
| }); |
| |
| var RootTextBlock = P(RootMathBlock, function(_, super_) { |
| _.keystroke = function(key) { |
| if (key === 'Spacebar' || key === 'Shift-Spacebar') return; |
| return super_.keystroke.apply(this, arguments); |
| }; |
| _.write = function(cursor, ch) { |
| cursor.show().deleteSelection(); |
| if (ch === '$') |
| RootMathCommand(cursor).createLeftOf(cursor); |
| else { |
| var html; |
| if (ch === '<') html = '<'; |
| else if (ch === '>') html = '>'; |
| VanillaSymbol(ch, html).createLeftOf(cursor); |
| } |
| }; |
| }); |
| API.TextField = function(APIClasses) { |
| return P(APIClasses.EditableField, function(_, super_) { |
| this.RootBlock = RootTextBlock; |
| _.__mathquillify = function() { |
| return super_.__mathquillify.call(this, 'mq-editable-field mq-text-mode'); |
| }; |
| _.latex = function(latex) { |
| if (arguments.length > 0) { |
| this.__controller.renderLatexText(latex); |
| if (this.__controller.blurred) this.__controller.cursor.hide().parent.blur(); |
| return this; |
| } |
| return this.__controller.exportLatex(); |
| }; |
| }); |
| }; |
| /**************************************** |
| * Input box to type backslash commands |
| ***************************************/ |
| |
| var LatexCommandInput = |
| CharCmds['\\'] = P(MathCommand, function(_, super_) { |
| _.ctrlSeq = '\\'; |
| _.replaces = function(replacedFragment) { |
| this._replacedFragment = replacedFragment.disown(); |
| this.isEmpty = function() { return false; }; |
| }; |
| _.htmlTemplate = '<span class="mq-latex-command-input mq-non-leaf">\\<span>&0</span></span>'; |
| _.textTemplate = ['\\']; |
| _.createBlocks = function() { |
| super_.createBlocks.call(this); |
| this.ends[L].focus = function() { |
| this.parent.jQ.addClass('mq-hasCursor'); |
| if (this.isEmpty()) |
| this.parent.jQ.removeClass('mq-empty'); |
| |
| return this; |
| }; |
| this.ends[L].blur = function() { |
| this.parent.jQ.removeClass('mq-hasCursor'); |
| if (this.isEmpty()) |
| this.parent.jQ.addClass('mq-empty'); |
| |
| return this; |
| }; |
| this.ends[L].write = function(cursor, ch) { |
| cursor.show().deleteSelection(); |
| |
| if (ch.match(/[a-z]/i)) VanillaSymbol(ch).createLeftOf(cursor); |
| else { |
| this.parent.renderCommand(cursor); |
| if (ch !== '\\' || !this.isEmpty()) cursor.parent.write(cursor, ch); |
| } |
| }; |
| this.ends[L].keystroke = function(key, e, ctrlr) { |
| if (key === 'Tab' || key === 'Enter' || key === 'Spacebar') { |
| this.parent.renderCommand(ctrlr.cursor); |
| e.preventDefault(); |
| return; |
| } |
| return super_.keystroke.apply(this, arguments); |
| }; |
| }; |
| _.createLeftOf = function(cursor) { |
| super_.createLeftOf.call(this, cursor); |
| |
| if (this._replacedFragment) { |
| var el = this.jQ[0]; |
| this.jQ = |
| this._replacedFragment.jQ.addClass('mq-blur').bind( |
| 'mousedown mousemove', //FIXME: is monkey-patching the mousedown and mousemove handlers the right way to do this? |
| function(e) { |
| $(e.target = el).trigger(e); |
| return false; |
| } |
| ).insertBefore(this.jQ).add(this.jQ); |
| } |
| }; |
| _.latex = function() { |
| return '\\' + this.ends[L].latex() + ' '; |
| }; |
| _.renderCommand = function(cursor) { |
| this.jQ = this.jQ.last(); |
| this.remove(); |
| if (this[R]) { |
| cursor.insLeftOf(this[R]); |
| } else { |
| cursor.insAtRightEnd(this.parent); |
| } |
| |
| var latex = this.ends[L].latex(); |
| if (!latex) latex = ' '; |
| var cmd = LatexCmds[latex] || Environments[latex]; |
| if (cmd) { |
| cmd = cmd(latex); |
| if (this._replacedFragment) cmd.replaces(this._replacedFragment); |
| cmd.createLeftOf(cursor); |
| } |
| else { |
| cmd = TextBlock(); |
| cmd.replaces(latex); |
| cmd.createLeftOf(cursor); |
| cursor.insRightOf(cmd); |
| if (this._replacedFragment) |
| this._replacedFragment.remove(); |
| } |
| }; |
| }); |
| |
| /*************************** |
| * Commands and Operators. |
| **************************/ |
| |
| var scale, // = function(jQ, x, y) { ... } |
| //will use a CSS 2D transform to scale the jQuery-wrapped HTML elements, |
| //or the filter matrix transform fallback for IE 5.5-8, or gracefully degrade to |
| //increasing the fontSize to match the vertical Y scaling factor. |
| |
| //ideas from http://github.com/louisremi/jquery.transform.js |
| //see also http://msdn.microsoft.com/en-us/library/ms533014(v=vs.85).aspx |
| |
| forceIERedraw = noop, |
| div = document.createElement('div'), |
| div_style = div.style, |
| transformPropNames = { |
| transform:1, |
| WebkitTransform:1, |
| MozTransform:1, |
| OTransform:1, |
| msTransform:1 |
| }, |
| transformPropName; |
| |
| for (var prop in transformPropNames) { |
| if (prop in div_style) { |
| transformPropName = prop; |
| break; |
| } |
| } |
| |
| if (transformPropName) { |
| scale = function(jQ, x, y) { |
| jQ.css(transformPropName, 'scale('+x+','+y+')'); |
| }; |
| } |
| else if ('filter' in div_style) { //IE 6, 7, & 8 fallback, see https://github.com/laughinghan/mathquill/wiki/Transforms |
| forceIERedraw = function(el){ el.className = el.className; }; |
| scale = function(jQ, x, y) { //NOTE: assumes y > x |
| x /= (1+(y-1)/2); |
| jQ.css('fontSize', y + 'em'); |
| if (!jQ.hasClass('mq-matrixed-container')) { |
| jQ.addClass('mq-matrixed-container') |
| .wrapInner('<span class="mq-matrixed"></span>'); |
| } |
| var innerjQ = jQ.children() |
| .css('filter', 'progid:DXImageTransform.Microsoft' |
| + '.Matrix(M11=' + x + ",SizingMethod='auto expand')" |
| ); |
| function calculateMarginRight() { |
| jQ.css('marginRight', (innerjQ.width()-1)*(x-1)/x + 'px'); |
| } |
| calculateMarginRight(); |
| var intervalId = setInterval(calculateMarginRight); |
| $(window).load(function() { |
| clearTimeout(intervalId); |
| calculateMarginRight(); |
| }); |
| }; |
| } |
| else { |
| scale = function(jQ, x, y) { |
| jQ.css('fontSize', y + 'em'); |
| }; |
| } |
| |
| var Style = P(MathCommand, function(_, super_) { |
| _.init = function(ctrlSeq, tagName, attrs) { |
| super_.init.call(this, ctrlSeq, '<'+tagName+' '+attrs+'>&0</'+tagName+'>'); |
| }; |
| }); |
| |
| //fonts |
| LatexCmds.mathrm = bind(Style, '\\mathrm', 'span', 'class="mq-roman mq-font"'); |
| LatexCmds.mathit = bind(Style, '\\mathit', 'i', 'class="mq-font"'); |
| LatexCmds.mathbf = bind(Style, '\\mathbf', 'b', 'class="mq-font"'); |
| LatexCmds.mathsf = bind(Style, '\\mathsf', 'span', 'class="mq-sans-serif mq-font"'); |
| LatexCmds.mathtt = bind(Style, '\\mathtt', 'span', 'class="mq-monospace mq-font"'); |
| //text-decoration |
| LatexCmds.underline = bind(Style, '\\underline', 'span', 'class="mq-non-leaf mq-underline"'); |
| LatexCmds.overline = LatexCmds.bar = bind(Style, '\\overline', 'span', 'class="mq-non-leaf mq-overline"'); |
| LatexCmds.overrightarrow = bind(Style, '\\overrightarrow', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-right"'); |
| LatexCmds.overleftarrow = bind(Style, '\\overleftarrow', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-left"'); |
| LatexCmds.overleftrightarrow = bind(Style, '\\overleftrightarrow', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-both"'); |
| LatexCmds.overarc = bind(Style, '\\overarc', 'span', 'class="mq-non-leaf mq-overarc"'); |
| LatexCmds.dot = P(MathCommand, function(_, super_) { |
| _.init = function() { |
| super_.init.call(this, '\\dot', '<span class="mq-non-leaf"><span class="mq-dot-recurring-inner">' |
| + '<span class="mq-dot-recurring">˙</span>' |
| + '<span class="mq-empty-box">&0</span>' |
| + '</span></span>' |
| ); |
| }; |
| }); |
| |
| // `\textcolor{color}{math}` will apply a color to the given math content, where |
| // `color` is any valid CSS Color Value (see [SitePoint docs][] (recommended), |
| // [Mozilla docs][], or [W3C spec][]). |
| // |
| // [SitePoint docs]: http://reference.sitepoint.com/css/colorvalues |
| // [Mozilla docs]: https://developer.mozilla.org/en-US/docs/CSS/color_value#Values |
| // [W3C spec]: http://dev.w3.org/csswg/css3-color/#colorunits |
| var TextColor = LatexCmds.textcolor = P(MathCommand, function(_, super_) { |
| _.setColor = function(color) { |
| this.color = color; |
| this.htmlTemplate = |
| '<span class="mq-textcolor" style="color:' + color + '">&0</span>'; |
| }; |
| _.latex = function() { |
| return '\\textcolor{' + this.color + '}{' + this.blocks[0].latex() + '}'; |
| }; |
| _.parser = function() { |
| var self = this; |
| var optWhitespace = Parser.optWhitespace; |
| var string = Parser.string; |
| var regex = Parser.regex; |
| |
| return optWhitespace |
| .then(string('{')) |
| .then(regex(/^[#\w\s.,()%-]*/)) |
| .skip(string('}')) |
| .then(function(color) { |
| self.setColor(color); |
| return super_.parser.call(self); |
| }) |
| ; |
| }; |
| _.isStyleBlock = function() { |
| return true; |
| }; |
| }); |
| |
| // Very similar to the \textcolor command, but will add the given CSS class. |
| // Usage: \class{classname}{math} |
| // Note regex that whitelists valid CSS classname characters: |
| // https://github.com/mathquill/mathquill/pull/191#discussion_r4327442 |
| var Class = LatexCmds['class'] = P(MathCommand, function(_, super_) { |
| _.parser = function() { |
| var self = this, string = Parser.string, regex = Parser.regex; |
| return Parser.optWhitespace |
| .then(string('{')) |
| .then(regex(/^[-\w\s\\\xA0-\xFF]*/)) |
| .skip(string('}')) |
| .then(function(cls) { |
| self.cls = cls || ''; |
| self.htmlTemplate = '<span class="mq-class '+cls+'">&0</span>'; |
| return super_.parser.call(self); |
| }) |
| ; |
| }; |
| _.latex = function() { |
| return '\\class{' + this.cls + '}{' + this.blocks[0].latex() + '}'; |
| }; |
| _.isStyleBlock = function() { |
| return true; |
| }; |
| }); |
| |
| var SupSub = P(MathCommand, function(_, super_) { |
| _.ctrlSeq = '_{...}^{...}'; |
| _.createLeftOf = function(cursor) { |
| if (!this.replacedFragment && !cursor[L] && cursor.options.supSubsRequireOperand) return; |
| return super_.createLeftOf.apply(this, arguments); |
| }; |
| _.contactWeld = function(cursor) { |
| // Look on either side for a SupSub, if one is found compare my |
| // .sub, .sup with its .sub, .sup. If I have one that it doesn't, |
| // then call .addBlock() on it with my block; if I have one that |
| // it also has, then insert my block's children into its block, |
| // unless my block has none, in which case insert the cursor into |
| // its block (and not mine, I'm about to remove myself) in the case |
| // I was just typed. |
| // TODO: simplify |
| |
| // equiv. to [L, R].forEach(function(dir) { ... }); |
| for (var dir = L; dir; dir = (dir === L ? R : false)) { |
| if (this[dir] instanceof SupSub) { |
| // equiv. to 'sub sup'.split(' ').forEach(function(supsub) { ... }); |
| for (var supsub = 'sub'; supsub; supsub = (supsub === 'sub' ? 'sup' : false)) { |
| var src = this[supsub], dest = this[dir][supsub]; |
| if (!src) continue; |
| if (!dest) this[dir].addBlock(src.disown()); |
| else if (!src.isEmpty()) { // ins src children at -dir end of dest |
| src.jQ.children().insAtDirEnd(-dir, dest.jQ); |
| var children = src.children().disown(); |
| var pt = Point(dest, children.ends[R], dest.ends[L]); |
| if (dir === L) children.adopt(dest, dest.ends[R], 0); |
| else children.adopt(dest, 0, dest.ends[L]); |
| } |
| else var pt = Point(dest, 0, dest.ends[L]); |
| this.placeCursor = (function(dest, src) { // TODO: don't monkey-patch |
| return function(cursor) { cursor.insAtDirEnd(-dir, dest || src); }; |
| }(dest, src)); |
| } |
| this.remove(); |
| if (cursor && cursor[L] === this) { |
| if (dir === R && pt) { |
| pt[L] ? cursor.insRightOf(pt[L]) : cursor.insAtLeftEnd(pt.parent); |
| } |
| else cursor.insRightOf(this[dir]); |
| } |
| break; |
| } |
| } |
| }; |
| Options.p.charsThatBreakOutOfSupSub = ''; |
| _.finalizeTree = function() { |
| this.ends[L].write = function(cursor, ch) { |
| if (cursor.options.autoSubscriptNumerals && this === this.parent.sub) { |
| if (ch === '_') return; |
| var cmd = this.chToCmd(ch, cursor.options); |
| if (cmd instanceof Symbol) cursor.deleteSelection(); |
| else cursor.clearSelection().insRightOf(this.parent); |
| return cmd.createLeftOf(cursor.show()); |
| } |
| if (cursor[L] && !cursor[R] && !cursor.selection |
| && cursor.options.charsThatBreakOutOfSupSub.indexOf(ch) > -1) { |
| cursor.insRightOf(this.parent); |
| } |
| MathBlock.p.write.apply(this, arguments); |
| }; |
| }; |
| _.moveTowards = function(dir, cursor, updown) { |
| if (cursor.options.autoSubscriptNumerals && !this.sup) { |
| cursor.insDirOf(dir, this); |
| } |
| else super_.moveTowards.apply(this, arguments); |
| }; |
| _.deleteTowards = function(dir, cursor) { |
| if (cursor.options.autoSubscriptNumerals && this.sub) { |
| var cmd = this.sub.ends[-dir]; |
| if (cmd instanceof Symbol) cmd.remove(); |
| else if (cmd) cmd.deleteTowards(dir, cursor.insAtDirEnd(-dir, this.sub)); |
| |
| // TODO: factor out a .removeBlock() or something |
| if (this.sub.isEmpty()) { |
| this.sub.deleteOutOf(L, cursor.insAtLeftEnd(this.sub)); |
| if (this.sup) cursor.insDirOf(-dir, this); |
| // Note `-dir` because in e.g. x_1^2| want backspacing (leftward) |
| // to delete the 1 but to end up rightward of x^2; with non-negated |
| // `dir` (try it), the cursor appears to have gone "through" the ^2. |
| } |
| } |
| else super_.deleteTowards.apply(this, arguments); |
| }; |
| _.latex = function() { |
| function latex(prefix, block) { |
| var l = block && block.latex(); |
| return block ? prefix + (l.length === 1 ? l : '{' + (l || ' ') + '}') : ''; |
| } |
| return latex('_', this.sub) + latex('^', this.sup); |
| }; |
| _.addBlock = function(block) { |
| if (this.supsub === 'sub') { |
| this.sup = this.upInto = this.sub.upOutOf = block; |
| block.adopt(this, this.sub, 0).downOutOf = this.sub; |
| block.jQ = $('<span class="mq-sup"/>').append(block.jQ.children()) |
| .attr(mqBlockId, block.id).prependTo(this.jQ); |
| } |
| else { |
| this.sub = this.downInto = this.sup.downOutOf = block; |
| block.adopt(this, 0, this.sup).upOutOf = this.sup; |
| block.jQ = $('<span class="mq-sub"></span>').append(block.jQ.children()) |
| .attr(mqBlockId, block.id).appendTo(this.jQ.removeClass('mq-sup-only')); |
| this.jQ.append('<span style="display:inline-block;width:0">​</span>'); |
| } |
| // like 'sub sup'.split(' ').forEach(function(supsub) { ... }); |
| for (var i = 0; i < 2; i += 1) (function(cmd, supsub, oppositeSupsub, updown) { |
| cmd[supsub].deleteOutOf = function(dir, cursor) { |
| cursor.insDirOf((this[dir] ? -dir : dir), this.parent); |
| if (!this.isEmpty()) { |
| var end = this.ends[dir]; |
| this.children().disown() |
| .withDirAdopt(dir, cursor.parent, cursor[dir], cursor[-dir]) |
| .jQ.insDirOf(-dir, cursor.jQ); |
| cursor[-dir] = end; |
| } |
| cmd.supsub = oppositeSupsub; |
| delete cmd[supsub]; |
| delete cmd[updown+'Into']; |
| cmd[oppositeSupsub][updown+'OutOf'] = insLeftOfMeUnlessAtEnd; |
| delete cmd[oppositeSupsub].deleteOutOf; |
| if (supsub === 'sub') $(cmd.jQ.addClass('mq-sup-only')[0].lastChild).remove(); |
| this.remove(); |
| }; |
| }(this, 'sub sup'.split(' ')[i], 'sup sub'.split(' ')[i], 'down up'.split(' ')[i])); |
| }; |
| }); |
| |
| function insLeftOfMeUnlessAtEnd(cursor) { |
| // cursor.insLeftOf(cmd), unless cursor at the end of block, and every |
| // ancestor cmd is at the end of every ancestor block |
| var cmd = this.parent, ancestorCmd = cursor; |
| do { |
| if (ancestorCmd[R]) return cursor.insLeftOf(cmd); |
| ancestorCmd = ancestorCmd.parent.parent; |
| } while (ancestorCmd !== cmd); |
| cursor.insRightOf(cmd); |
| } |
| |
| LatexCmds.subscript = |
| LatexCmds._ = P(SupSub, function(_, super_) { |
| _.supsub = 'sub'; |
| _.htmlTemplate = |
| '<span class="mq-supsub mq-non-leaf">' |
| + '<span class="mq-sub">&0</span>' |
| + '<span style="display:inline-block;width:0">​</span>' |
| + '</span>' |
| ; |
| _.textTemplate = [ '_' ]; |
| _.finalizeTree = function() { |
| this.downInto = this.sub = this.ends[L]; |
| this.sub.upOutOf = insLeftOfMeUnlessAtEnd; |
| super_.finalizeTree.call(this); |
| }; |
| }); |
| |
| LatexCmds.superscript = |
| LatexCmds.supscript = |
| LatexCmds['^'] = P(SupSub, function(_, super_) { |
| _.supsub = 'sup'; |
| _.htmlTemplate = |
| '<span class="mq-supsub mq-non-leaf mq-sup-only">' |
| + '<span class="mq-sup">&0</span>' |
| + '</span>' |
| ; |
| _.textTemplate = [ '^' ]; |
| _.finalizeTree = function() { |
| this.upInto = this.sup = this.ends[R]; |
| this.sup.downOutOf = insLeftOfMeUnlessAtEnd; |
| super_.finalizeTree.call(this); |
| }; |
| _.reflow = function() { |
| var $block = this.jQ;//mq-supsub |
| |
| var h = $block.prev().innerHeight() ; |
| h *= 0.6 ; |
| |
| $block.css( 'vertical-align', h + 'px' ) ; |
| |
| } ; |
| }); |
| |
| var SummationNotation = P(MathCommand, function(_, super_) { |
| _.init = function(ch, html) { |
| var htmlTemplate = |
| '<span class="mq-large-operator mq-non-leaf">' |
| + '<span class="mq-to"><span>&1</span></span>' |
| + '<big>'+html+'</big>' |
| + '<span class="mq-from"><span>&0</span></span>' |
| + '</span>' |
| ; |
| Symbol.prototype.init.call(this, ch, htmlTemplate); |
| }; |
| _.createLeftOf = function(cursor) { |
| super_.createLeftOf.apply(this, arguments); |
| if (cursor.options.sumStartsWithNEquals) { |
| Letter('n').createLeftOf(cursor); |
| Equality().createLeftOf(cursor); |
| } |
| }; |
| _.latex = function() { |
| function simplify(latex) { |
| return latex.length === 1 ? latex : '{' + (latex || ' ') + '}'; |
| } |
| return this.ctrlSeq + '_' + simplify(this.ends[L].latex()) + |
| '^' + simplify(this.ends[R].latex()); |
| }; |
| _.parser = function() { |
| var string = Parser.string; |
| var optWhitespace = Parser.optWhitespace; |
| var succeed = Parser.succeed; |
| var block = latexMathParser.block; |
| |
| var self = this; |
| var blocks = self.blocks = [ MathBlock(), MathBlock() ]; |
| for (var i = 0; i < blocks.length; i += 1) { |
| blocks[i].adopt(self, self.ends[R], 0); |
| } |
| |
| return optWhitespace.then(string('_').or(string('^'))).then(function(supOrSub) { |
| var child = blocks[supOrSub === '_' ? 0 : 1]; |
| return block.then(function(block) { |
| block.children().adopt(child, child.ends[R], 0); |
| return succeed(self); |
| }); |
| }).many().result(self); |
| }; |
| _.finalizeTree = function() { |
| this.downInto = this.ends[L]; |
| this.upInto = this.ends[R]; |
| this.ends[L].upOutOf = this.ends[R]; |
| this.ends[R].downOutOf = this.ends[L]; |
| }; |
| }); |
| |
| LatexCmds['\u2211'] = |
| LatexCmds.sum = |
| LatexCmds.summation = bind(SummationNotation,'\\sum ','∑'); |
| |
| LatexCmds['\u220f'] = |
| LatexCmds.prod = |
| LatexCmds.product = bind(SummationNotation,'\\prod ','∏'); |
| |
| LatexCmds.coprod = |
| LatexCmds.coproduct = bind(SummationNotation,'\\coprod ','∐'); |
| |
| LatexCmds['\u222b'] = |
| LatexCmds['int'] = |
| LatexCmds.integral = P(SummationNotation, function(_, super_) { |
| _.init = function() { |
| var htmlTemplate = |
| '<span class="mq-int mq-non-leaf">' |
| + '<big>∫</big>' |
| + '<span class="mq-supsub mq-non-leaf">' |
| + '<span class="mq-sup"><span class="mq-sup-inner">&1</span></span>' |
| + '<span class="mq-sub">&0</span>' |
| + '<span style="display:inline-block;width:0">​</span>' |
| + '</span>' |
| + '</span>' |
| ; |
| Symbol.prototype.init.call(this, '\\int ', htmlTemplate); |
| }; |
| // FIXME: refactor rather than overriding |
| _.createLeftOf = MathCommand.p.createLeftOf; |
| }); |
| |
| var Fraction = |
| LatexCmds.frac = |
| LatexCmds.dfrac = |
| LatexCmds.cfrac = |
| LatexCmds.fraction = P(MathCommand, function(_, super_) { |
| _.ctrlSeq = '\\frac'; |
| _.htmlTemplate = |
| '<span class="mq-fraction mq-non-leaf">' |
| + '<span class="mq-numerator">&0</span>' |
| + '<span class="mq-denominator">&1</span>' |
| + '<span style="display:inline-block;width:0">​</span>' |
| + '</span>' |
| ; |
| _.textTemplate = ['(', ')/(', ')']; |
| _.finalizeTree = function() { |
| this.upInto = this.ends[R].upOutOf = this.ends[L]; |
| this.downInto = this.ends[L].downOutOf = this.ends[R]; |
| }; |
| }); |
| |
| var LiveFraction = |
| LatexCmds.over = |
| CharCmds['/'] = P(Fraction, function(_, super_) { |
| _.createLeftOf = function(cursor) { |
| if (!this.replacedFragment) { |
| var leftward = cursor[L]; |
| while (leftward && |
| !( |
| leftward instanceof BinaryOperator || |
| leftward instanceof (LatexCmds.text || noop) || |
| leftward instanceof SummationNotation || |
| leftward.ctrlSeq === '\\ ' || |
| /^[,;:]$/.test(leftward.ctrlSeq) |
| ) //lookbehind for operator |
| ) leftward = leftward[L]; |
| |
| if (leftward instanceof SummationNotation && leftward[R] instanceof SupSub) { |
| leftward = leftward[R]; |
| if (leftward[R] instanceof SupSub && leftward[R].ctrlSeq != leftward.ctrlSeq) |
| leftward = leftward[R]; |
| } |
| |
| if (leftward !== cursor[L]) { |
| this.replaces(Fragment(leftward[R] || cursor.parent.ends[L], cursor[L])); |
| cursor[L] = leftward; |
| } |
| } |
| super_.createLeftOf.call(this, cursor); |
| }; |
| }); |
| |
| var SquareRoot = |
| LatexCmds.sqrt = |
| LatexCmds['\u221a'] = P(MathCommand, function(_, super_) { |
| _.ctrlSeq = '\\sqrt'; |
| _.htmlTemplate = |
| '<span class="mq-non-leaf">' |
| + '<span class="mq-scaled mq-sqrt-prefix">√</span>' |
| + '<span class="mq-non-leaf mq-sqrt-stem">&0</span>' |
| + '</span>' |
| ; |
| _.textTemplate = ['sqrt(', ')']; |
| _.parser = function() { |
| return latexMathParser.optBlock.then(function(optBlock) { |
| return latexMathParser.block.map(function(block) { |
| var nthroot = NthRoot(); |
| nthroot.blocks = [ optBlock, block ]; |
| optBlock.adopt(nthroot, 0, 0); |
| block.adopt(nthroot, optBlock, 0); |
| return nthroot; |
| }); |
| }).or(super_.parser.call(this)); |
| }; |
| _.reflow = function() { |
| var block = this.ends[R].jQ; |
| scale(block.prev(), 1, block.innerHeight()/+block.css('fontSize').slice(0,-2) - .1); |
| }; |
| }); |
| |
| var Hat = LatexCmds.hat = P(MathCommand, function(_, super_) { |
| _.ctrlSeq = '\\hat'; |
| _.htmlTemplate = |
| '<span class="mq-non-leaf">' |
| + '<span class="mq-hat-prefix">^</span>' |
| + '<span class="mq-hat-stem">&0</span>' |
| + '</span>' |
| ; |
| _.textTemplate = ['hat(', ')']; |
| }); |
| |
| var NthRoot = |
| LatexCmds.nthroot = P(SquareRoot, function(_, super_) { |
| _.htmlTemplate = |
| '<sup class="mq-nthroot mq-non-leaf">&0</sup>' |
| + '<span class="mq-scaled">' |
| + '<span class="mq-sqrt-prefix mq-scaled">√</span>' |
| + '<span class="mq-sqrt-stem mq-non-leaf">&1</span>' |
| + '</span>' |
| ; |
| _.textTemplate = ['sqrt[', '](', ')']; |
| _.latex = function() { |
| return '\\sqrt['+this.ends[L].latex()+']{'+this.ends[R].latex()+'}'; |
| }; |
| }); |
| |
| var DiacriticAbove = P(MathCommand, function(_, super_) { |
| _.init = function(ctrlSeq, symbol, textTemplate) { |
| var htmlTemplate = |
| '<span class="mq-non-leaf">' |
| + '<span class="mq-diacritic-above">'+symbol+'</span>' |
| + '<span class="mq-diacritic-stem">&0</span>' |
| + '</span>' |
| ; |
| |
| super_.init.call(this, ctrlSeq, htmlTemplate, textTemplate); |
| }; |
| }); |
| LatexCmds.vec = bind(DiacriticAbove, '\\vec', '→', ['vec(', ')']); |
| LatexCmds.tilde = bind(DiacriticAbove, '\\tilde', '~', ['tilde(', ')']); |
| |
| function DelimsMixin(_, super_) { |
| _.jQadd = function() { |
| super_.jQadd.apply(this, arguments); |
| this.delimjQs = this.jQ.children(':first').add(this.jQ.children(':last')); |
| this.contentjQ = this.jQ.children(':eq(1)'); |
| }; |
| _.reflow = function() { |
| var height = this.contentjQ.outerHeight() |
| / parseFloat(this.contentjQ.css('fontSize')); |
| scale(this.delimjQs, min(1 + .2*(height - 1), 1.2), 1.2*height); |
| }; |
| } |
| |
| // Round/Square/Curly/Angle Brackets (aka Parens/Brackets/Braces) |
| // first typed as one-sided bracket with matching "ghost" bracket at |
| // far end of current block, until you type an opposing one |
| var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) { |
| _.init = function(side, open, close, ctrlSeq, end) { |
| super_.init.call(this, '\\left'+ctrlSeq, undefined, [open, close]); |
| this.side = side; |
| this.sides = {}; |
| this.sides[L] = { ch: open, ctrlSeq: ctrlSeq }; |
| this.sides[R] = { ch: close, ctrlSeq: end }; |
| }; |
| _.numBlocks = function() { return 1; }; |
| _.html = function() { // wait until now so that .side may |
| this.htmlTemplate = // be set by createLeftOf or parser |
| '<span class="mq-non-leaf">' |
| + '<span class="mq-scaled mq-paren'+(this.side === R ? ' mq-ghost' : '')+'">' |
| + this.sides[L].ch |
| + '</span>' |
| + '<span class="mq-non-leaf">&0</span>' |
| + '<span class="mq-scaled mq-paren'+(this.side === L ? ' mq-ghost' : '')+'">' |
| + this.sides[R].ch |
| + '</span>' |
| + '</span>' |
| ; |
| return super_.html.call(this); |
| }; |
| _.latex = function() { |
| return '\\left'+this.sides[L].ctrlSeq+this.ends[L].latex()+'\\right'+this.sides[R].ctrlSeq; |
| }; |
| _.matchBrack = function(opts, expectedSide, node) { |
| // return node iff it's a matching 1-sided bracket of expected side (if any) |
| return node instanceof Bracket && node.side && node.side !== -expectedSide |
| && (!opts.restrictMismatchedBrackets |
| || OPP_BRACKS[this.sides[this.side].ch] === node.sides[node.side].ch |
| || { '(': ']', '[': ')' }[this.sides[L].ch] === node.sides[R].ch) && node; |
| }; |
| _.closeOpposing = function(brack) { |
| brack.side = 0; |
| brack.sides[this.side] = this.sides[this.side]; // copy over my info (may be |
| brack.delimjQs.eq(this.side === L ? 0 : 1) // mismatched, like [a, b)) |
| .removeClass('mq-ghost').html(this.sides[this.side].ch); |
| }; |
| _.createLeftOf = function(cursor) { |
| if (!this.replacedFragment) { // unless wrapping seln in brackets, |
| // check if next to or inside an opposing one-sided bracket |
| var opts = cursor.options; |
| if (this.sides[L].ch === '|') { // check both sides if I'm a pipe |
| var brack = this.matchBrack(opts, R, cursor[R]) |
| || this.matchBrack(opts, L, cursor[L]) |
| || this.matchBrack(opts, 0, cursor.parent.parent); |
| } |
| else { |
| var brack = this.matchBrack(opts, -this.side, cursor[-this.side]) |
| || this.matchBrack(opts, -this.side, cursor.parent.parent); |
| } |
| } |
| if (brack) { |
| var side = this.side = -brack.side; // may be pipe with .side not yet set |
| this.closeOpposing(brack); |
| if (brack === cursor.parent.parent && cursor[side]) { // move the stuff between |
| Fragment(cursor[side], cursor.parent.ends[side], -side) // me and ghost outside |
| .disown().withDirAdopt(-side, brack.parent, brack, brack[side]) |
| .jQ.insDirOf(side, brack.jQ); |
| } |
| brack.bubble('reflow'); |
| } |
| else { |
| brack = this, side = brack.side; |
| if (brack.replacedFragment) brack.side = 0; // wrapping seln, don't be one-sided |
| else if (cursor[-side]) { // elsewise, auto-expand so ghost is at far end |
| brack.replaces(Fragment(cursor[-side], cursor.parent.ends[-side], side)); |
| cursor[-side] = 0; |
| } |
| super_.createLeftOf.call(brack, cursor); |
| } |
| if (side === L) cursor.insAtLeftEnd(brack.ends[L]); |
| else cursor.insRightOf(brack); |
| }; |
| _.placeCursor = noop; |
| _.unwrap = function() { |
| this.ends[L].children().disown().adopt(this.parent, this, this[R]) |
| .jQ.insertAfter(this.jQ); |
| this.remove(); |
| }; |
| _.deleteSide = function(side, outward, cursor) { |
| var parent = this.parent, sib = this[side], farEnd = parent.ends[side]; |
| |
| if (side === this.side) { // deleting non-ghost of one-sided bracket, unwrap |
| this.unwrap(); |
| sib ? cursor.insDirOf(-side, sib) : cursor.insAtDirEnd(side, parent); |
| return; |
| } |
| |
| var opts = cursor.options, wasSolid = !this.side; |
| this.side = -side; |
| // if deleting like, outer close-brace of [(1+2)+3} where inner open-paren |
| if (this.matchBrack(opts, side, this.ends[L].ends[this.side])) { // is ghost, |
| this.closeOpposing(this.ends[L].ends[this.side]); // then become [1+2)+3 |
| var origEnd = this.ends[L].ends[side]; |
| this.unwrap(); |
| if (origEnd.siblingCreated) origEnd.siblingCreated(cursor.options, side); |
| sib ? cursor.insDirOf(-side, sib) : cursor.insAtDirEnd(side, parent); |
| } |
| else { // if deleting like, inner close-brace of ([1+2}+3) where outer |
| if (this.matchBrack(opts, side, this.parent.parent)) { // open-paren is |
| this.parent.parent.closeOpposing(this); // ghost, then become [1+2+3) |
| this.parent.parent.unwrap(); |
| } // else if deleting outward from a solid pair, unwrap |
| else if (outward && wasSolid) { |
| this.unwrap(); |
| sib ? cursor.insDirOf(-side, sib) : cursor.insAtDirEnd(side, parent); |
| return; |
| } |
| else { // else deleting just one of a pair of brackets, become one-sided |
| this.sides[side] = { ch: OPP_BRACKS[this.sides[this.side].ch], |
| ctrlSeq: OPP_BRACKS[this.sides[this.side].ctrlSeq] }; |
| this.delimjQs.removeClass('mq-ghost') |
| .eq(side === L ? 0 : 1).addClass('mq-ghost').html(this.sides[side].ch); |
| } |
| if (sib) { // auto-expand so ghost is at far end |
| var origEnd = this.ends[L].ends[side]; |
| Fragment(sib, farEnd, -side).disown() |
| .withDirAdopt(-side, this.ends[L], origEnd, 0) |
| .jQ.insAtDirEnd(side, this.ends[L].jQ.removeClass('mq-empty')); |
| if (origEnd.siblingCreated) origEnd.siblingCreated(cursor.options, side); |
| cursor.insDirOf(-side, sib); |
| } // didn't auto-expand, cursor goes just outside or just inside parens |
| else (outward ? cursor.insDirOf(side, this) |
| : cursor.insAtDirEnd(side, this.ends[L])); |
| } |
| }; |
| _.deleteTowards = function(dir, cursor) { |
| this.deleteSide(-dir, false, cursor); |
| }; |
| _.finalizeTree = function() { |
| this.ends[L].deleteOutOf = function(dir, cursor) { |
| this.parent.deleteSide(dir, true, cursor); |
| }; |
| // FIXME HACK: after initial creation/insertion, finalizeTree would only be |
| // called if the paren is selected and replaced, e.g. by LiveFraction |
| this.finalizeTree = this.intentionalBlur = function() { |
| this.delimjQs.eq(this.side === L ? 1 : 0).removeClass('mq-ghost'); |
| this.side = 0; |
| }; |
| }; |
| _.siblingCreated = function(opts, dir) { // if something typed between ghost and far |
| if (dir === -this.side) this.finalizeTree(); // end of its block, solidify |
| }; |
| }); |
| |
| var OPP_BRACKS = { |
| '(': ')', |
| ')': '(', |
| '[': ']', |
| ']': '[', |
| '{': '}', |
| '}': '{', |
| '\\{': '\\}', |
| '\\}': '\\{', |
| '⟨': '⟩', |
| '⟩': '⟨', |
| '\\langle ': '\\rangle ', |
| '\\rangle ': '\\langle ', |
| '|': '|', |
| '\\lVert ' : '\\rVert ', |
| '\\rVert ' : '\\lVert ', |
| }; |
| |
| function bindCharBracketPair(open, ctrlSeq) { |
| var ctrlSeq = ctrlSeq || open, close = OPP_BRACKS[open], end = OPP_BRACKS[ctrlSeq]; |
| CharCmds[open] = bind(Bracket, L, open, close, ctrlSeq, end); |
| CharCmds[close] = bind(Bracket, R, open, close, ctrlSeq, end); |
| } |
| bindCharBracketPair('('); |
| bindCharBracketPair('['); |
| bindCharBracketPair('{', '\\{'); |
| LatexCmds.langle = bind(Bracket, L, '⟨', '⟩', '\\langle ', '\\rangle '); |
| LatexCmds.rangle = bind(Bracket, R, '⟨', '⟩', '\\langle ', '\\rangle '); |
| CharCmds['|'] = bind(Bracket, L, '|', '|', '|', '|'); |
| LatexCmds.lVert = bind(Bracket, L, '∥', '∥', '\\lVert ', '\\rVert '); |
| LatexCmds.rVert = bind(Bracket, R, '∥', '∥', '\\lVert ', '\\rVert '); |
| |
| LatexCmds.left = P(MathCommand, function(_) { |
| _.parser = function() { |
| var regex = Parser.regex; |
| var string = Parser.string; |
| var succeed = Parser.succeed; |
| var optWhitespace = Parser.optWhitespace; |
| |
| return optWhitespace.then(regex(/^(?:[([|]|\\\{|\\langle\b|\\lVert\b)/)) |
| .then(function(ctrlSeq) { |
| var open = (ctrlSeq.charAt(0) === '\\' ? ctrlSeq.slice(1) : ctrlSeq); |
| if (ctrlSeq=="\\langle") { open = '⟨'; ctrlSeq = ctrlSeq + ' '; } |
| if (ctrlSeq=="\\lVert") { open = '∥'; ctrlSeq = ctrlSeq + ' '; } |
| return latexMathParser.then(function (block) { |
| return string('\\right').skip(optWhitespace) |
| .then(regex(/^(?:[\])|]|\\\}|\\rangle\b|\\rVert\b)/)).map(function(end) { |
| var close = (end.charAt(0) === '\\' ? end.slice(1) : end); |
| if (end=="\\rangle") { close = '⟩'; end = end + ' '; } |
| if (end=="\\rVert") { close = '∥'; end = end + ' '; } |
| var cmd = Bracket(0, open, close, ctrlSeq, end); |
| cmd.blocks = [ block ]; |
| block.adopt(cmd, 0, 0); |
| return cmd; |
| }) |
| ; |
| }); |
| }) |
| ; |
| }; |
| }); |
| |
| LatexCmds.right = P(MathCommand, function(_) { |
| _.parser = function() { |
| return Parser.fail('unmatched \\right'); |
| }; |
| }); |
| |
| var Binomial = |
| LatexCmds.binom = |
| LatexCmds.binomial = P(P(MathCommand, DelimsMixin), function(_, super_) { |
| _.ctrlSeq = '\\binom'; |
| _.htmlTemplate = |
| '<span class="mq-non-leaf">' |
| + '<span class="mq-paren mq-scaled">(</span>' |
| + '<span class="mq-non-leaf">' |
| + '<span class="mq-array mq-non-leaf">' |
| + '<span>&0</span>' |
| + '<span>&1</span>' |
| + '</span>' |
| + '</span>' |
| + '<span class="mq-paren mq-scaled">)</span>' |
| + '</span>' |
| ; |
| _.textTemplate = ['choose(',',',')']; |
| }); |
| |
| var Choose = |
| LatexCmds.choose = P(Binomial, function(_) { |
| _.createLeftOf = LiveFraction.prototype.createLeftOf; |
| }); |
| |
| LatexCmds.editable = // backcompat with before cfd3620 on #233 |
| LatexCmds.MathQuillMathField = P(MathCommand, function(_, super_) { |
| _.ctrlSeq = '\\MathQuillMathField'; |
| _.htmlTemplate = |
| '<span class="mq-editable-field">' |
| + '<span class="mq-root-block">&0</span>' |
| + '</span>' |
| ; |
| _.parser = function() { |
| var self = this, |
| string = Parser.string, regex = Parser.regex, succeed = Parser.succeed; |
| return string('[').then(regex(/^[a-z][a-z0-9]*/i)).skip(string(']')) |
| .map(function(name) { self.name = name; }).or(succeed()) |
| .then(super_.parser.call(self)); |
| }; |
| _.finalizeTree = function(options) { |
| var ctrlr = Controller(this.ends[L], this.jQ, options); |
| ctrlr.KIND_OF_MQ = 'MathField'; |
| ctrlr.editable = true; |
| ctrlr.createTextarea(); |
| ctrlr.editablesTextareaEvents(); |
| ctrlr.cursor.insAtRightEnd(ctrlr.root); |
| RootBlockMixin(ctrlr.root); |
| }; |
| _.registerInnerField = function(innerFields, MathField) { |
| innerFields.push(innerFields[this.name] = MathField(this.ends[L].controller)); |
| }; |
| _.latex = function(){ return this.ends[L].latex(); }; |
| _.text = function(){ return this.ends[L].text(); }; |
| }); |
| |
| // Embed arbitrary things |
| // Probably the closest DOM analogue would be an iframe? |
| // From MathQuill's perspective, it's a Symbol, it can be |
| // anywhere and the cursor can go around it but never in it. |
| // Create by calling public API method .dropEmbedded(), |
| // or by calling the global public API method .registerEmbed() |
| // and rendering LaTeX like \embed{registeredName} (see test). |
| var Embed = LatexCmds.embed = P(Symbol, function(_, super_) { |
| _.setOptions = function(options) { |
| function noop () { return ""; } |
| this.text = options.text || noop; |
| this.htmlTemplate = options.htmlString || ""; |
| this.latex = options.latex || noop; |
| return this; |
| }; |
| _.parser = function() { |
| var self = this, |
| string = Parser.string, regex = Parser.regex, succeed = Parser.succeed; |
| return string('{').then(regex(/^[a-z][a-z0-9]*/i)).skip(string('}')) |
| .then(function(name) { |
| // the chars allowed in the optional data block are arbitrary other than |
| // excluding curly braces and square brackets (which'd be too confusing) |
| return string('[').then(regex(/^[-\w\s]*/)).skip(string(']')) |
| .or(succeed()).map(function(data) { |
| return self.setOptions(EMBEDS[name](data)); |
| }) |
| ; |
| }) |
| ; |
| }; |
| }); |
| |
| // LaTeX environments |
| // Environments are delimited by an opening \begin{} and a closing |
| // \end{}. Everything inside those tags will be formatted in a |
| // special manner depending on the environment type. |
| var Environments = {}; |
| |
| LatexCmds.begin = P(MathCommand, function(_, super_) { |
| _.parser = function() { |
| var string = Parser.string; |
| var regex = Parser.regex; |
| return string('{') |
| .then(regex(/^[a-z]+/i)) |
| .skip(string('}')) |
| .then(function (env) { |
| return (Environments[env] ? |
| Environments[env]().parser() : |
| Parser.fail('unknown environment type: '+env) |
| ).skip(string('\\end{'+env+'}')); |
| }) |
| ; |
| }; |
| }); |
| |
| var Environment = P(MathCommand, function(_, super_) { |
| _.template = [['\\begin{', '}'], ['\\end{', '}']]; |
| _.wrappers = function () { |
| return [ |
| _.template[0].join(this.environment), |
| _.template[1].join(this.environment) |
| ]; |
| }; |
| }); |
| |
| var Matrix = |
| Environments.matrix = P(Environment, function(_, super_) { |
| |
| var delimiters = { |
| column: '&', |
| row: '\\\\' |
| }; |
| _.parentheses = { |
| left: null, |
| right: null |
| }; |
| _.environment = 'matrix'; |
| |
| _.reflow = function() { |
| var blockjQ = this.jQ.children('table'); |
| |
| var height = blockjQ.outerHeight()/+blockjQ.css('fontSize').slice(0,-2); |
| |
| var parens = this.jQ.children('.mq-paren'); |
| if (parens.length) { |
| scale(parens, min(1 + .2*(height - 1), 1.2), 1.05*height); |
| } |
| }; |
| _.latex = function() { |
| var latex = ''; |
| var row; |
| |
| this.eachChild(function (cell) { |
| if (typeof row !== 'undefined') { |
| latex += (row !== cell.row) ? |
| delimiters.row : |
| delimiters.column; |
| } |
| row = cell.row; |
| latex += cell.latex(); |
| }); |
| |
| return this.wrappers().join(latex); |
| }; |
| _.html = function() { |
| var cells = [], trs = '', i=0, row; |
| |
| function parenHtml(paren) { |
| return (paren) ? |
| '<span class="mq-scaled mq-paren">' |
| + paren |
| + '</span>' : ''; |
| } |
| |
| // Build <tr><td>.. structure from cells |
| this.eachChild(function (cell) { |
| if (row !== cell.row) { |
| row = cell.row; |
| trs += '<tr>$tds</tr>'; |
| cells[row] = []; |
| } |
| cells[row].push('<td>&'+(i++)+'</td>'); |
| }); |
| |
| this.htmlTemplate = |
| '<span class="mq-matrix mq-non-leaf">' |
| + parenHtml(this.parentheses.left) |
| + '<table class="mq-non-leaf">' |
| + trs.replace(/\$tds/g, function () { |
| return cells.shift().join(''); |
| }) |
| + '</table>' |
| + parenHtml(this.parentheses.right) |
| + '</span>' |
| ; |
| |
| return super_.html.call(this); |
| }; |
| // Create default 4-cell matrix |
| _.createBlocks = function() { |
| this.blocks = [ |
| MatrixCell(0, this), |
| MatrixCell(0, this), |
| MatrixCell(1, this), |
| MatrixCell(1, this) |
| ]; |
| }; |
| _.parser = function() { |
| var self = this; |
| var optWhitespace = Parser.optWhitespace; |
| var string = Parser.string; |
| |
| return optWhitespace |
| .then(string(delimiters.column) |
| .or(string(delimiters.row)) |
| .or(latexMathParser.block)) |
| .many() |
| .skip(optWhitespace) |
| .then(function(items) { |
| var blocks = []; |
| var row = 0; |
| self.blocks = []; |
| |
| function addCell() { |
| self.blocks.push(MatrixCell(row, self, blocks)); |
| blocks = []; |
| } |
| |
| for (var i=0; i<items.length; i+=1) { |
| if (items[i] instanceof MathBlock) { |
| blocks.push(items[i]); |
| } else { |
| addCell(); |
| if (items[i] === delimiters.row) row+=1; |
| } |
| } |
| addCell(); |
| self.autocorrect(); |
| return Parser.succeed(self); |
| }); |
| }; |
| // Relink all the cells after parsing |
| _.finalizeTree = function() { |
| var table = this.jQ.find('table'); |
| table.toggleClass('mq-rows-1', table.find('tr').length === 1); |
| this.relink(); |
| }; |
| // Enter the matrix at the top or bottom row if updown is configured. |
| _.getEntryPoint = function(dir, cursor, updown) { |
| if (updown === 'up') { |
| if (dir === L) { |
| return this.blocks[this.rowSize - 1]; |
| } else { |
| return this.blocks[0]; |
| } |
| } else { // updown === 'down' |
| if (dir === L) { |
| return this.blocks[this.blocks.length - 1]; |
| } else { |
| return this.blocks[this.blocks.length - this.rowSize]; |
| } |
| } |
| }; |
| // Exit the matrix at the first and last columns if updown is configured. |
| _.atExitPoint = function(dir, cursor) { |
| // Which block are we in? |
| var i = this.blocks.indexOf(cursor.parent); |
| if (dir === L) { |
| // If we're on the left edge and moving left, we should exit. |
| return i % this.rowSize === 0; |
| } else { |
| // If we're on the right edge and moving right, we should exit. |
| return (i + 1) % this.rowSize === 0; |
| } |
| }; |
| _.moveTowards = function(dir, cursor, updown) { |
| var entryPoint = updown && this.getEntryPoint(dir, cursor, updown); |
| cursor.insAtDirEnd(-dir, entryPoint || this.ends[-dir]); |
| }; |
| |
| // Set up directional pointers between cells |
| _.relink = function() { |
| var blocks = this.blocks; |
| var rows = []; |
| var row, column, cell; |
| |
| // The row size will be used by other functions down the track. |
| // Begin by assuming we're a one-row matrix, and we'll overwrite this if we find another row. |
| this.rowSize = blocks.length; |
| |
| // Use a for loop rather than eachChild |
| // as we're still making sure children() |
| // is set up properly |
| for (var i=0; i<blocks.length; i+=1) { |
| cell = blocks[i]; |
| if (row !== cell.row) { |
| if (cell.row === 1) { |
| // We've just finished iterating the first row. |
| this.rowSize = column; |
| } |
| row = cell.row; |
| rows[row] = []; |
| column = 0; |
| } |
| rows[row][column] = cell; |
| |
| // Set up horizontal linkage |
| cell[R] = blocks[i+1]; |
| cell[L] = blocks[i-1]; |
| |
| // Set up vertical linkage |
| if (rows[row-1] && rows[row-1][column]) { |
| cell.upOutOf = rows[row-1][column]; |
| rows[row-1][column].downOutOf = cell; |
| } |
| |
| column+=1; |
| } |
| |
| // set start and end blocks of matrix |
| this.ends[L] = blocks[0]; |
| this.ends[R] = blocks[blocks.length-1]; |
| }; |
| // Ensure consistent row lengths |
| _.autocorrect = function(rows) { |
| var lengths = [], rows = []; |
| var blocks = this.blocks; |
| var maxLength, shortfall, position, row, i; |
| |
| for (i=0; i<blocks.length; i+=1) { |
| row = blocks[i].row; |
| rows[row] = rows[row] || []; |
| rows[row].push(blocks[i]); |
| lengths[row] = rows[row].length; |
| } |
| |
| maxLength = Math.max.apply(null, lengths); |
| if (maxLength !== Math.min.apply(null, lengths)) { |
| // Pad shorter rows to correct length |
| for (i=0; i<rows.length; i+=1) { |
| shortfall = maxLength - rows[i].length; |
| while (shortfall) { |
| position = maxLength*i + rows[i].length; |
| blocks.splice(position, 0, MatrixCell(i, this)); |
| shortfall-=1; |
| } |
| } |
| this.relink(); |
| } |
| }; |
| // Deleting a cell will also delete the current row and |
| // column if they are empty, and relink the matrix. |
| _.deleteCell = function(currentCell) { |
| var rows = [], columns = [], myRow = [], myColumn = []; |
| var blocks = this.blocks, row, column; |
| |
| // Create arrays for cells in the current row / column |
| this.eachChild(function (cell) { |
| if (row !== cell.row) { |
| row = cell.row; |
| rows[row] = []; |
| column = 0; |
| } |
| columns[column] = columns[column] || []; |
| columns[column].push(cell); |
| rows[row].push(cell); |
| |
| if (cell === currentCell) { |
| myRow = rows[row]; |
| myColumn = columns[column]; |
| } |
| |
| column+=1; |
| }); |
| |
| function isEmpty(cells) { |
| var empties = []; |
| for (var i=0; i<cells.length; i+=1) { |
| if (cells[i].isEmpty()) empties.push(cells[i]); |
| } |
| return empties.length === cells.length; |
| } |
| |
| function remove(cells) { |
| for (var i=0; i<cells.length; i+=1) { |
| if (blocks.indexOf(cells[i]) > -1) { |
| cells[i].remove(); |
| blocks.splice(blocks.indexOf(cells[i]), 1); |
| } |
| } |
| } |
| |
| if (isEmpty(myRow) && myColumn.length > 1) { |
| row = rows.indexOf(myRow); |
| // Decrease all following row numbers |
| this.eachChild(function (cell) { |
| if (cell.row > row) cell.row-=1; |
| }); |
| // Dispose of cells and remove <tr> |
| remove(myRow); |
| this.jQ.find('tr').eq(row).remove(); |
| } |
| if (isEmpty(myColumn) && myRow.length > 1) { |
| remove(myColumn); |
| } |
| this.finalizeTree(); |
| }; |
| _.addRow = function(afterCell) { |
| var previous = [], newCells = [], next = []; |
| var newRow = $('<tr></tr>'), row = afterCell.row; |
| var columns = 0, block, column; |
| |
| this.eachChild(function (cell) { |
| // Cache previous rows |
| if (cell.row <= row) { |
| previous.push(cell); |
| } |
| // Work out how many columns |
| if (cell.row === row) { |
| if (cell === afterCell) column = columns; |
| columns+=1; |
| } |
| // Cache cells after new row |
| if (cell.row > row) { |
| cell.row+=1; |
| next.push(cell); |
| } |
| }); |
| |
| // Add new cells, one for each column |
| for (var i=0; i<columns; i+=1) { |
| block = MatrixCell(row+1); |
| block.parent = this; |
| newCells.push(block); |
| |
| // Create cell <td>s and add to new row |
| block.jQ = $('<td class="mq-empty">') |
| .attr(mqBlockId, block.id) |
| .appendTo(newRow); |
| } |
| |
| // Insert the new row |
| this.jQ.find('tr').eq(row).after(newRow); |
| this.blocks = previous.concat(newCells, next); |
| return newCells[column]; |
| }; |
| _.addColumn = function(afterCell) { |
| var rows = [], newCells = []; |
| var column, block; |
| |
| // Build rows array and find new column index |
| this.eachChild(function (cell) { |
| rows[cell.row] = rows[cell.row] || []; |
| rows[cell.row].push(cell); |
| if (cell === afterCell) column = rows[cell.row].length; |
| }); |
| |
| // Add new cells, one for each row |
| for (var i=0; i<rows.length; i+=1) { |
| block = MatrixCell(i); |
| block.parent = this; |
| newCells.push(block); |
| rows[i].splice(column, 0, block); |
| |
| block.jQ = $('<td class="mq-empty">') |
| .attr(mqBlockId, block.id); |
| } |
| |
| // Add cell <td> elements in correct positions |
| this.jQ.find('tr').each(function (i) { |
| $(this).find('td').eq(column-1).after(rows[i][column].jQ); |
| }); |
| |
| // Flatten the rows array-of-arrays |
| this.blocks = [].concat.apply([], rows); |
| return newCells[afterCell.row]; |
| }; |
| _.insert = function(method, afterCell) { |
| var cellToFocus = this[method](afterCell); |
| this.cursor = this.cursor || this.parent.cursor; |
| this.finalizeTree(); |
| this.bubble('reflow').cursor.insAtRightEnd(cellToFocus); |
| }; |
| _.backspace = function(cell, dir, cursor, finalDeleteCallback) { |
| var dirwards = cell[dir]; |
| if (cell.isEmpty()) { |
| this.deleteCell(cell); |
| while (dirwards && |
| dirwards[dir] && |
| this.blocks.indexOf(dirwards) === -1) { |
| dirwards = dirwards[dir]; |
| } |
| if (dirwards) { |
| cursor.insAtDirEnd(-dir, dirwards); |
| } |
| if (this.blocks.length === 1 && this.blocks[0].isEmpty()) { |
| finalDeleteCallback(); |
| this.finalizeTree(); |
| } |
| this.bubble('edited'); |
| } |
| }; |
| }); |
| |
| Environments.pmatrix = P(Matrix, function(_, super_) { |
| _.environment = 'pmatrix'; |
| _.parentheses = { |
| left: '(', |
| right: ')' |
| }; |
| }); |
| |
| Environments.bmatrix = P(Matrix, function(_, super_) { |
| _.environment = 'bmatrix'; |
| _.parentheses = { |
| left: '[', |
| right: ']' |
| }; |
| }); |
| |
| Environments.Bmatrix = P(Matrix, function(_, super_) { |
| _.environment = 'Bmatrix'; |
| _.parentheses = { |
| left: '{', |
| right: '}' |
| }; |
| }); |
| |
| Environments.vmatrix = P(Matrix, function(_, super_) { |
| _.environment = 'vmatrix'; |
| _.parentheses = { |
| left: '|', |
| right: '|' |
| }; |
| }); |
| |
| Environments.Vmatrix = P(Matrix, function(_, super_) { |
| _.environment = 'Vmatrix'; |
| _.parentheses = { |
| left: '‖', |
| right: '‖' |
| }; |
| }); |
| |
| // Replacement for mathblocks inside matrix cells |
| // Adds matrix-specific keyboard commands |
| var MatrixCell = P(MathBlock, function(_, super_) { |
| _.init = function(row, parent, replaces) { |
| super_.init.call(this); |
| this.row = row; |
| if (parent) { |
| this.adopt(parent, parent.ends[R], 0); |
| } |
| if (replaces) { |
| for (var i=0; i<replaces.length; i++) { |
| replaces[i].children().adopt(this, this.ends[R], 0); |
| } |
| } |
| }; |
| _.keystroke = function(key, e, ctrlr) { |
| switch (key) { |
| case 'Shift-Spacebar': |
| e.preventDefault(); |
| return this.parent.insert('addColumn', this); |
| break; |
| case 'Shift-Enter': |
| return this.parent.insert('addRow', this); |
| break; |
| } |
| return super_.keystroke.apply(this, arguments); |
| }; |
| _.deleteOutOf = function(dir, cursor) { |
| var self = this, args = arguments; |
| this.parent.backspace(this, dir, cursor, function () { |
| // called when last cell gets deleted |
| return super_.deleteOutOf.apply(self, args); |
| }); |
| }; |
| _.moveOutOf = function(dir, cursor, updown) { |
| var atExitPoint = updown && this.parent.atExitPoint(dir, cursor); |
| // Step out of the matrix if we've moved past an edge column |
| if (!atExitPoint && this[dir]) cursor.insAtDirEnd(-dir, this[dir]); |
| else cursor.insDirOf(dir, this.parent); |
| }; |
| }); |
| /************************************ |
| * Symbols for Advanced Mathematics |
| ***********************************/ |
| |
| LatexCmds.notin = |
| LatexCmds.cong = |
| LatexCmds.equiv = |
| LatexCmds.oplus = |
| LatexCmds.otimes = P(BinaryOperator, function(_, super_) { |
| _.init = function(latex) { |
| super_.init.call(this, '\\'+latex+' ', '&'+latex+';'); |
| }; |
| }); |
| |
| LatexCmds['\u2260'] = LatexCmds.ne = LatexCmds.neq = bind(BinaryOperator,'\\ne ','≠'); |
| |
| LatexCmds['\u2217'] = LatexCmds.ast = LatexCmds.star = LatexCmds.loast = LatexCmds.lowast = |
| bind(BinaryOperator,'\\ast ','∗'); |
| |
| LatexCmds.therefor = LatexCmds.therefore = |
| bind(BinaryOperator,'\\therefore ','∴'); |
| |
| LatexCmds.cuz = // l33t |
| LatexCmds.because = bind(BinaryOperator,'\\because ','∵'); |
| |
| LatexCmds.prop = LatexCmds.propto = bind(BinaryOperator,'\\propto ','∝'); |
| |
| LatexCmds['\u2248'] = LatexCmds.asymp = LatexCmds.approx = bind(BinaryOperator,'\\approx ','≈'); |
| |
| LatexCmds.isin = LatexCmds['in'] = bind(BinaryOperator,'\\in ','∈'); |
| |
| LatexCmds.ni = LatexCmds.contains = bind(BinaryOperator,'\\ni ','∋'); |
| |
| LatexCmds.notni = LatexCmds.niton = LatexCmds.notcontains = LatexCmds.doesnotcontain = |
| bind(BinaryOperator,'\\not\\ni ','∌'); |
| |
| LatexCmds.sub = LatexCmds.subset = bind(BinaryOperator,'\\subset ','⊂'); |
| |
| LatexCmds.sup = LatexCmds.supset = LatexCmds.superset = |
| bind(BinaryOperator,'\\supset ','⊃'); |
| |
| LatexCmds.nsub = LatexCmds.notsub = |
| LatexCmds.nsubset = LatexCmds.notsubset = |
| bind(BinaryOperator,'\\not\\subset ','⊄'); |
| |
| LatexCmds.nsup = LatexCmds.notsup = |
| LatexCmds.nsupset = LatexCmds.notsupset = |
| LatexCmds.nsuperset = LatexCmds.notsuperset = |
| bind(BinaryOperator,'\\not\\supset ','⊅'); |
| |
| LatexCmds.sube = LatexCmds.subeq = LatexCmds.subsete = LatexCmds.subseteq = |
| bind(BinaryOperator,'\\subseteq ','⊆'); |
| |
| LatexCmds.supe = LatexCmds.supeq = |
| LatexCmds.supsete = LatexCmds.supseteq = |
| LatexCmds.supersete = LatexCmds.superseteq = |
| bind(BinaryOperator,'\\supseteq ','⊇'); |
| |
| LatexCmds.nsube = LatexCmds.nsubeq = |
| LatexCmds.notsube = LatexCmds.notsubeq = |
| LatexCmds.nsubsete = LatexCmds.nsubseteq = |
| LatexCmds.notsubsete = LatexCmds.notsubseteq = |
| bind(BinaryOperator,'\\not\\subseteq ','⊈'); |
| |
| LatexCmds.nsupe = LatexCmds.nsupeq = |
| LatexCmds.notsupe = LatexCmds.notsupeq = |
| LatexCmds.nsupsete = LatexCmds.nsupseteq = |
| LatexCmds.notsupsete = LatexCmds.notsupseteq = |
| LatexCmds.nsupersete = LatexCmds.nsuperseteq = |
| LatexCmds.notsupersete = LatexCmds.notsuperseteq = |
| bind(BinaryOperator,'\\not\\supseteq ','⊉'); |
| |
| |
| //the canonical sets of numbers |
| LatexCmds.N = LatexCmds.naturals = LatexCmds.Naturals = |
| bind(VanillaSymbol,'\\mathbb{N}','ℕ'); |
| |
| LatexCmds.P = |
| LatexCmds.primes = LatexCmds.Primes = |
| LatexCmds.projective = LatexCmds.Projective = |
| LatexCmds.probability = LatexCmds.Probability = |
| bind(VanillaSymbol,'\\mathbb{P}','ℙ'); |
| |
| LatexCmds.Z = LatexCmds.integers = LatexCmds.Integers = |
| bind(VanillaSymbol,'\\mathbb{Z}','ℤ'); |
| |
| LatexCmds.Q = LatexCmds.rationals = LatexCmds.Rationals = |
| bind(VanillaSymbol,'\\mathbb{Q}','ℚ'); |
| |
| LatexCmds.R = LatexCmds.reals = LatexCmds.Reals = |
| bind(VanillaSymbol,'\\mathbb{R}','ℝ'); |
| |
| LatexCmds.C = |
| LatexCmds.complex = LatexCmds.Complex = |
| LatexCmds.complexes = LatexCmds.Complexes = |
| LatexCmds.complexplane = LatexCmds.Complexplane = LatexCmds.ComplexPlane = |
| bind(VanillaSymbol,'\\mathbb{C}','ℂ'); |
| |
| LatexCmds.H = LatexCmds.Hamiltonian = LatexCmds.quaternions = LatexCmds.Quaternions = |
| bind(VanillaSymbol,'\\mathbb{H}','ℍ'); |
| |
| //spacing |
| LatexCmds.quad = LatexCmds.emsp = bind(VanillaSymbol,'\\quad ',' '); |
| LatexCmds.qquad = bind(VanillaSymbol,'\\qquad ',' '); |
| /* spacing special characters, gonna have to implement this in LatexCommandInput::onText somehow |
| case ',': |
| return VanillaSymbol('\\, ',' '); |
| case ':': |
| return VanillaSymbol('\\: ',' '); |
| case ';': |
| return VanillaSymbol('\\; ',' '); |
| case '!': |
| return Symbol('\\! ','<span style="margin-right:-.2em"></span>'); |
| */ |
| |
| //binary operators |
| LatexCmds.diamond = bind(VanillaSymbol, '\\diamond ', '◇'); |
| LatexCmds.bigtriangleup = bind(VanillaSymbol, '\\bigtriangleup ', '△'); |
| LatexCmds.ominus = bind(VanillaSymbol, '\\ominus ', '⊖'); |
| LatexCmds.uplus = bind(VanillaSymbol, '\\uplus ', '⊎'); |
| LatexCmds.bigtriangledown = bind(VanillaSymbol, '\\bigtriangledown ', '▽'); |
| LatexCmds.sqcap = bind(VanillaSymbol, '\\sqcap ', '⊓'); |
| LatexCmds.triangleleft = bind(VanillaSymbol, '\\triangleleft ', '⊲'); |
| LatexCmds.sqcup = bind(VanillaSymbol, '\\sqcup ', '⊔'); |
| LatexCmds.triangleright = bind(VanillaSymbol, '\\triangleright ', '⊳'); |
| //circledot is not a not real LaTex command see https://github.com/mathquill/mathquill/pull/552 for more details |
| LatexCmds.odot = LatexCmds.circledot = bind(VanillaSymbol, '\\odot ', '⊙'); |
| LatexCmds.bigcirc = bind(VanillaSymbol, '\\bigcirc ', '◯'); |
| LatexCmds.dagger = bind(VanillaSymbol, '\\dagger ', '†'); |
| LatexCmds.ddagger = bind(VanillaSymbol, '\\ddagger ', '‡'); |
| LatexCmds.wr = bind(VanillaSymbol, '\\wr ', '≀'); |
| LatexCmds.amalg = bind(VanillaSymbol, '\\amalg ', '∐'); |
| |
| //relationship symbols |
| LatexCmds.models = bind(VanillaSymbol, '\\models ', '⊨'); |
| LatexCmds.prec = bind(VanillaSymbol, '\\prec ', '≺'); |
| LatexCmds.succ = bind(VanillaSymbol, '\\succ ', '≻'); |
| LatexCmds.preceq = bind(VanillaSymbol, '\\preceq ', '≼'); |
| LatexCmds.succeq = bind(VanillaSymbol, '\\succeq ', '≽'); |
| LatexCmds.simeq = bind(VanillaSymbol, '\\simeq ', '≃'); |
| LatexCmds.mid = bind(VanillaSymbol, '\\mid ', '∣'); |
| LatexCmds.ll = bind(VanillaSymbol, '\\ll ', '≪'); |
| LatexCmds.gg = bind(VanillaSymbol, '\\gg ', '≫'); |
| LatexCmds.parallel = bind(VanillaSymbol, '\\parallel ', '∥'); |
| LatexCmds.nparallel = bind(VanillaSymbol, '\\nparallel ', '∦'); |
| LatexCmds.bowtie = bind(VanillaSymbol, '\\bowtie ', '⋈'); |
| LatexCmds.sqsubset = bind(VanillaSymbol, '\\sqsubset ', '⊏'); |
| LatexCmds.sqsupset = bind(VanillaSymbol, '\\sqsupset ', '⊐'); |
| LatexCmds.smile = bind(VanillaSymbol, '\\smile ', '⌣'); |
| LatexCmds.sqsubseteq = bind(VanillaSymbol, '\\sqsubseteq ', '⊑'); |
| LatexCmds.sqsupseteq = bind(VanillaSymbol, '\\sqsupseteq ', '⊒'); |
| LatexCmds.doteq = bind(VanillaSymbol, '\\doteq ', '≐'); |
| LatexCmds.frown = bind(VanillaSymbol, '\\frown ', '⌢'); |
| LatexCmds.vdash = bind(VanillaSymbol, '\\vdash ', '⊦'); |
| LatexCmds.dashv = bind(VanillaSymbol, '\\dashv ', '⊣'); |
| LatexCmds.nless = bind(VanillaSymbol, '\\nless ', '≮'); |
| LatexCmds.ngtr = bind(VanillaSymbol, '\\ngtr ', '≯'); |
| |
| //arrows |
| LatexCmds.longleftarrow = bind(VanillaSymbol, '\\longleftarrow ', '←'); |
| LatexCmds.longrightarrow = bind(VanillaSymbol, '\\longrightarrow ', '→'); |
| LatexCmds.Longleftarrow = bind(VanillaSymbol, '\\Longleftarrow ', '⇐'); |
| LatexCmds.Longrightarrow = bind(VanillaSymbol, '\\Longrightarrow ', '⇒'); |
| LatexCmds.longleftrightarrow = bind(VanillaSymbol, '\\longleftrightarrow ', '↔'); |
| LatexCmds.updownarrow = bind(VanillaSymbol, '\\updownarrow ', '↕'); |
| LatexCmds.Longleftrightarrow = bind(VanillaSymbol, '\\Longleftrightarrow ', '⇔'); |
| LatexCmds.Updownarrow = bind(VanillaSymbol, '\\Updownarrow ', '⇕'); |
| LatexCmds.mapsto = bind(VanillaSymbol, '\\mapsto ', '↦'); |
| LatexCmds.nearrow = bind(VanillaSymbol, '\\nearrow ', '↗'); |
| LatexCmds.hookleftarrow = bind(VanillaSymbol, '\\hookleftarrow ', '↩'); |
| LatexCmds.hookrightarrow = bind(VanillaSymbol, '\\hookrightarrow ', '↪'); |
| LatexCmds.searrow = bind(VanillaSymbol, '\\searrow ', '↘'); |
| LatexCmds.leftharpoonup = bind(VanillaSymbol, '\\leftharpoonup ', '↼'); |
| LatexCmds.rightharpoonup = bind(VanillaSymbol, '\\rightharpoonup ', '⇀'); |
| LatexCmds.swarrow = bind(VanillaSymbol, '\\swarrow ', '↙'); |
| LatexCmds.leftharpoondown = bind(VanillaSymbol, '\\leftharpoondown ', '↽'); |
| LatexCmds.rightharpoondown = bind(VanillaSymbol, '\\rightharpoondown ', '⇁'); |
| LatexCmds.nwarrow = bind(VanillaSymbol, '\\nwarrow ', '↖'); |
| |
| //Misc |
| LatexCmds.ldots = bind(VanillaSymbol, '\\ldots ', '…'); |
| LatexCmds.cdots = bind(VanillaSymbol, '\\cdots ', '⋯'); |
| LatexCmds.vdots = bind(VanillaSymbol, '\\vdots ', '⋮'); |
| LatexCmds.ddots = bind(VanillaSymbol, '\\ddots ', '⋱'); |
| LatexCmds.surd = bind(VanillaSymbol, '\\surd ', '√'); |
| LatexCmds.triangle = bind(VanillaSymbol, '\\triangle ', '△'); |
| LatexCmds.ell = bind(VanillaSymbol, '\\ell ', 'ℓ'); |
| LatexCmds.top = bind(VanillaSymbol, '\\top ', '⊤'); |
| LatexCmds.flat = bind(VanillaSymbol, '\\flat ', '♭'); |
| LatexCmds.natural = bind(VanillaSymbol, '\\natural ', '♮'); |
| LatexCmds.sharp = bind(VanillaSymbol, '\\sharp ', '♯'); |
| LatexCmds.wp = bind(VanillaSymbol, '\\wp ', '℘'); |
| LatexCmds.bot = bind(VanillaSymbol, '\\bot ', '⊥'); |
| LatexCmds.clubsuit = bind(VanillaSymbol, '\\clubsuit ', '♣'); |
| LatexCmds.diamondsuit = bind(VanillaSymbol, '\\diamondsuit ', '♢'); |
| LatexCmds.heartsuit = bind(VanillaSymbol, '\\heartsuit ', '♡'); |
| LatexCmds.spadesuit = bind(VanillaSymbol, '\\spadesuit ', '♠'); |
| //not real LaTex command see https://github.com/mathquill/mathquill/pull/552 for more details |
| LatexCmds.parallelogram = bind(VanillaSymbol, '\\parallelogram ', '▱'); |
| LatexCmds.square = bind(VanillaSymbol, '\\square ', '⬜'); |
| |
| //variable-sized |
| LatexCmds.oint = bind(VanillaSymbol, '\\oint ', '∮'); |
| LatexCmds.bigcap = bind(VanillaSymbol, '\\bigcap ', '∩'); |
| LatexCmds.bigcup = bind(VanillaSymbol, '\\bigcup ', '∪'); |
| LatexCmds.bigsqcup = bind(VanillaSymbol, '\\bigsqcup ', '⊔'); |
| LatexCmds.bigvee = bind(VanillaSymbol, '\\bigvee ', '∨'); |
| LatexCmds.bigwedge = bind(VanillaSymbol, '\\bigwedge ', '∧'); |
| LatexCmds.bigodot = bind(VanillaSymbol, '\\bigodot ', '⊙'); |
| LatexCmds.bigotimes = bind(VanillaSymbol, '\\bigotimes ', '⊗'); |
| LatexCmds.bigoplus = bind(VanillaSymbol, '\\bigoplus ', '⊕'); |
| LatexCmds.biguplus = bind(VanillaSymbol, '\\biguplus ', '⊎'); |
| |
| //delimiters |
| LatexCmds.lfloor = bind(VanillaSymbol, '\\lfloor ', '⌊'); |
| LatexCmds.rfloor = bind(VanillaSymbol, '\\rfloor ', '⌋'); |
| LatexCmds.lceil = bind(VanillaSymbol, '\\lceil ', '⌈'); |
| LatexCmds.rceil = bind(VanillaSymbol, '\\rceil ', '⌉'); |
| LatexCmds.opencurlybrace = LatexCmds.lbrace = bind(VanillaSymbol, '\\lbrace ', '{'); |
| LatexCmds.closecurlybrace = LatexCmds.rbrace = bind(VanillaSymbol, '\\rbrace ', '}'); |
| LatexCmds.lbrack = bind(VanillaSymbol, '['); |
| LatexCmds.rbrack = bind(VanillaSymbol, ']'); |
| |
| //various symbols |
| LatexCmds.slash = bind(VanillaSymbol, '/'); |
| LatexCmds.vert = bind(VanillaSymbol,'|'); |
| LatexCmds.perp = LatexCmds.perpendicular = bind(VanillaSymbol,'\\perp ','⊥'); |
| LatexCmds.nabla = LatexCmds.del = bind(VanillaSymbol,'\\nabla ','∇'); |
| LatexCmds.hbar = bind(VanillaSymbol,'\\hbar ','ℏ'); |
| |
| LatexCmds.AA = LatexCmds.Angstrom = LatexCmds.angstrom = |
| bind(VanillaSymbol,'\\text\\AA ','Å'); |
| |
| LatexCmds.ring = LatexCmds.circ = LatexCmds.circle = |
| bind(VanillaSymbol,'\\circ ','∘'); |
| |
| LatexCmds.bull = LatexCmds.bullet = bind(VanillaSymbol,'\\bullet ','•'); |
| |
| LatexCmds.setminus = LatexCmds.smallsetminus = |
| bind(VanillaSymbol,'\\setminus ','∖'); |
| |
| LatexCmds.not = //bind(Symbol,'\\not ','<span class="not">/</span>'); |
| LatexCmds['\u00ac'] = LatexCmds.neg = bind(VanillaSymbol,'\\neg ','¬'); |
| |
| LatexCmds['\u2026'] = LatexCmds.dots = LatexCmds.ellip = LatexCmds.hellip = |
| LatexCmds.ellipsis = LatexCmds.hellipsis = |
| bind(VanillaSymbol,'\\dots ','…'); |
| |
| LatexCmds.converges = |
| LatexCmds.darr = LatexCmds.dnarr = LatexCmds.dnarrow = LatexCmds.downarrow = |
| bind(VanillaSymbol,'\\downarrow ','↓'); |
| |
| LatexCmds.dArr = LatexCmds.dnArr = LatexCmds.dnArrow = LatexCmds.Downarrow = |
| bind(VanillaSymbol,'\\Downarrow ','⇓'); |
| |
| LatexCmds.diverges = LatexCmds.uarr = LatexCmds.uparrow = |
| bind(VanillaSymbol,'\\uparrow ','↑'); |
| |
| LatexCmds.uArr = LatexCmds.Uparrow = bind(VanillaSymbol,'\\Uparrow ','⇑'); |
| |
| LatexCmds.to = bind(BinaryOperator,'\\to ','→'); |
| |
| LatexCmds.rarr = LatexCmds.rightarrow = bind(VanillaSymbol,'\\rightarrow ','→'); |
| |
| LatexCmds.implies = bind(BinaryOperator,'\\Rightarrow ','⇒'); |
| |
| LatexCmds.rArr = LatexCmds.Rightarrow = bind(VanillaSymbol,'\\Rightarrow ','⇒'); |
| |
| LatexCmds.gets = bind(BinaryOperator,'\\gets ','←'); |
| |
| LatexCmds.larr = LatexCmds.leftarrow = bind(VanillaSymbol,'\\leftarrow ','←'); |
| |
| LatexCmds.impliedby = bind(BinaryOperator,'\\Leftarrow ','⇐'); |
| |
| LatexCmds.lArr = LatexCmds.Leftarrow = bind(VanillaSymbol,'\\Leftarrow ','⇐'); |
| |
| LatexCmds.harr = LatexCmds.lrarr = LatexCmds.leftrightarrow = |
| bind(VanillaSymbol,'\\leftrightarrow ','↔'); |
| |
| LatexCmds.iff = bind(BinaryOperator,'\\Leftrightarrow ','⇔'); |
| |
| LatexCmds.hArr = LatexCmds.lrArr = LatexCmds.Leftrightarrow = |
| bind(VanillaSymbol,'\\Leftrightarrow ','⇔'); |
| |
| LatexCmds.Re = LatexCmds.Real = LatexCmds.real = bind(VanillaSymbol,'\\Re ','ℜ'); |
| |
| LatexCmds.Im = LatexCmds.imag = |
| LatexCmds.image = LatexCmds.imagin = LatexCmds.imaginary = LatexCmds.Imaginary = |
| bind(VanillaSymbol,'\\Im ','ℑ'); |
| |
| LatexCmds.part = LatexCmds.partial = bind(VanillaSymbol,'\\partial ','∂'); |
| |
| LatexCmds.infty = LatexCmds.infin = LatexCmds.infinity = |
| bind(VanillaSymbol,'\\infty ','∞'); |
| |
| LatexCmds.alef = LatexCmds.alefsym = LatexCmds.aleph = LatexCmds.alephsym = |
| bind(VanillaSymbol,'\\aleph ','ℵ'); |
| |
| LatexCmds.xist = //LOL |
| LatexCmds.xists = LatexCmds.exist = LatexCmds.exists = |
| bind(VanillaSymbol,'\\exists ','∃'); |
| |
| LatexCmds.nexists = LatexCmds.nexist = |
| bind(VanillaSymbol, '\\nexists ', '∄'); |
| |
| LatexCmds.and = LatexCmds.land = LatexCmds.wedge = |
| bind(BinaryOperator,'\\wedge ','∧'); |
| |
| LatexCmds.or = LatexCmds.lor = LatexCmds.vee = bind(BinaryOperator,'\\vee ','∨'); |
| |
| LatexCmds.o = LatexCmds.O = |
| LatexCmds.empty = LatexCmds.emptyset = |
| LatexCmds.oslash = LatexCmds.Oslash = |
| LatexCmds.nothing = LatexCmds.varnothing = |
| bind(BinaryOperator,'\\varnothing ','∅'); |
| |
| LatexCmds.cup = LatexCmds.union = bind(BinaryOperator,'\\cup ','∪'); |
| |
| LatexCmds.cap = LatexCmds.intersect = LatexCmds.intersection = |
| bind(BinaryOperator,'\\cap ','∩'); |
| |
| // FIXME: the correct LaTeX would be ^\circ but we can't parse that |
| LatexCmds.deg = LatexCmds.degree = bind(VanillaSymbol,'\\degree ','°'); |
| |
| LatexCmds.ang = LatexCmds.angle = bind(VanillaSymbol,'\\angle ','∠'); |
| LatexCmds.measuredangle = bind(VanillaSymbol,'\\measuredangle ','∡'); |
| /********************************* |
| * Symbols for Basic Mathematics |
| ********************************/ |
| |
| var Digit = P(VanillaSymbol, function(_, super_) { |
| _.createLeftOf = function(cursor) { |
| if (cursor.options.autoSubscriptNumerals |
| && cursor.parent !== cursor.parent.parent.sub |
| && ((cursor[L] instanceof Variable && cursor[L].isItalic !== false) |
| || (cursor[L] instanceof SupSub |
| && cursor[L][L] instanceof Variable |
| && cursor[L][L].isItalic !== false))) { |
| LatexCmds._().createLeftOf(cursor); |
| super_.createLeftOf.call(this, cursor); |
| cursor.insRightOf(cursor.parent.parent); |
| } |
| else super_.createLeftOf.call(this, cursor); |
| }; |
| }); |
| |
| var Variable = P(Symbol, function(_, super_) { |
| _.init = function(ch, html) { |
| super_.init.call(this, ch, '<var>'+(html || ch)+'</var>'); |
| }; |
| _.text = function() { |
| var text = this.ctrlSeq; |
| if (this.isPartOfOperator) { |
| if (text[0] == '\\') { |
| text = text.slice(1, text.length); |
| } |
| else if (text[text.length-1] == ' ') { |
| text = text.slice (0, -1); |
| } |
| } else { |
| if (this[L] && !(this[L] instanceof Variable) |
| && !(this[L] instanceof BinaryOperator) |
| && this[L].ctrlSeq !== '\\ ') |
| text = '*' + text; |
| if (this[R] && !(this[R] instanceof BinaryOperator) |
| && !(this[R] instanceof SupSub)) |
| text += '*'; |
| } |
| return text; |
| }; |
| }); |
| |
| Options.p.autoCommands = { _maxLength: 0 }; |
| optionProcessors.autoCommands = function(cmds) { |
| if (!/^[a-z]+(?: [a-z]+)*$/i.test(cmds)) { |
| throw '"'+cmds+'" not a space-delimited list of only letters'; |
| } |
| var list = cmds.split(' '), dict = {}, maxLength = 0; |
| for (var i = 0; i < list.length; i += 1) { |
| var cmd = list[i]; |
| if (cmd.length < 2) { |
| throw 'autocommand "'+cmd+'" not minimum length of 2'; |
| } |
| if (LatexCmds[cmd] === OperatorName) { |
| throw '"' + cmd + '" is a built-in operator name'; |
| } |
| dict[cmd] = 1; |
| maxLength = max(maxLength, cmd.length); |
| } |
| dict._maxLength = maxLength; |
| return dict; |
| }; |
| |
| var Letter = P(Variable, function(_, super_) { |
| _.init = function(ch) { return super_.init.call(this, this.letter = ch); }; |
| _.createLeftOf = function(cursor) { |
| super_.createLeftOf.apply(this, arguments); |
| var autoCmds = cursor.options.autoCommands, maxLength = autoCmds._maxLength; |
| if (maxLength > 0) { |
| // want longest possible autocommand, so join together longest |
| // sequence of letters |
| var str = '', l = this, i = 0; |
| // FIXME: l.ctrlSeq === l.letter checks if first or last in an operator name |
| while (l instanceof Letter && l.ctrlSeq === l.letter && i < maxLength) { |
| str = l.letter + str, l = l[L], i += 1; |
| } |
| // check for an autocommand, going thru substrings longest to shortest |
| while (str.length) { |
| if (autoCmds.hasOwnProperty(str)) { |
| for (var i = 1, l = this; i < str.length; i += 1, l = l[L]); |
| Fragment(l, this).remove(); |
| cursor[L] = l[L]; |
| return LatexCmds[str](str).createLeftOf(cursor); |
| } |
| str = str.slice(1); |
| } |
| } |
| }; |
| _.italicize = function(bool) { |
| this.isItalic = bool; |
| this.isPartOfOperator = !bool; |
| this.jQ.toggleClass('mq-operator-name', !bool); |
| return this; |
| }; |
| _.finalizeTree = _.siblingDeleted = _.siblingCreated = function(opts, dir) { |
| // don't auto-un-italicize if the sibling to my right changed (dir === R or |
| // undefined) and it's now a Letter, it will un-italicize everyone |
| if (dir !== L && this[R] instanceof Letter) return; |
| this.autoUnItalicize(opts); |
| }; |
| _.autoUnItalicize = function(opts) { |
| var autoOps = opts.autoOperatorNames; |
| if (autoOps._maxLength === 0) return; |
| // want longest possible operator names, so join together entire contiguous |
| // sequence of letters |
| var str = this.letter; |
| for (var l = this[L]; l instanceof Letter; l = l[L]) str = l.letter + str; |
| for (var r = this[R]; r instanceof Letter; r = r[R]) str += r.letter; |
| |
| // removeClass and delete flags from all letters before figuring out |
| // which, if any, are part of an operator name |
| Fragment(l[R] || this.parent.ends[L], r[L] || this.parent.ends[R]).each(function(el) { |
| el.italicize(true).jQ.removeClass('mq-first mq-last mq-followed-by-supsub'); |
| el.ctrlSeq = el.letter; |
| }); |
| |
| // check for operator names: at each position from left to right, check |
| // substrings from longest to shortest |
| outer: for (var i = 0, first = l[R] || this.parent.ends[L]; i < str.length; i += 1, first = first[R]) { |
| for (var len = min(autoOps._maxLength, str.length - i); len > 0; len -= 1) { |
| var word = str.slice(i, i + len); |
| if (autoOps.hasOwnProperty(word)) { |
| for (var j = 0, letter = first; j < len; j += 1, letter = letter[R]) { |
| letter.italicize(false); |
| var last = letter; |
| } |
| |
| var isBuiltIn = BuiltInOpNames.hasOwnProperty(word); |
| first.ctrlSeq = (isBuiltIn ? '\\' : '\\operatorname{') + first.ctrlSeq; |
| last.ctrlSeq += (isBuiltIn ? ' ' : '}'); |
| if (TwoWordOpNames.hasOwnProperty(word)) last[L][L][L].jQ.addClass('mq-last'); |
| if (!shouldOmitPadding(first[L])) first.jQ.addClass('mq-first'); |
| if (!shouldOmitPadding(last[R])) { |
| if (last[R] instanceof SupSub) { |
| var supsub = last[R]; // XXX monkey-patching, but what's the right thing here? |
| // Have operatorname-specific code in SupSub? A CSS-like language to style the |
| // math tree, but which ignores cursor and selection (which CSS can't)? |
| var respace = supsub.siblingCreated = supsub.siblingDeleted = function() { |
| supsub.jQ.toggleClass('mq-after-operator-name', !(supsub[R] instanceof Bracket)); |
| }; |
| respace(); |
| } |
| else { |
| last.jQ.toggleClass('mq-last', !(last[R] instanceof Bracket)); |
| } |
| } |
| |
| i += len - 1; |
| first = last; |
| continue outer; |
| } |
| } |
| } |
| }; |
| function shouldOmitPadding(node) { |
| // omit padding if no node, or if node already has padding (to avoid double-padding) |
| return !node || (node instanceof BinaryOperator) || (node instanceof SummationNotation); |
| } |
| }); |
| var BuiltInOpNames = {}; // the set of operator names like \sin, \cos, etc that |
| // are built-into LaTeX, see Section 3.17 of the Short Math Guide: http://tinyurl.com/jm9okjc |
| // MathQuill auto-unitalicizes some operator names not in that set, like 'hcf' |
| // and 'arsinh', which must be exported as \operatorname{hcf} and |
| // \operatorname{arsinh}. Note: over/under line/arrow \lim variants like |
| // \varlimsup are not supported |
| var AutoOpNames = Options.p.autoOperatorNames = { _maxLength: 9 }; // the set |
| // of operator names that MathQuill auto-unitalicizes by default; overridable |
| var TwoWordOpNames = { limsup: 1, liminf: 1, projlim: 1, injlim: 1 }; |
| (function() { |
| var mostOps = ('arg deg det dim exp gcd hom inf ker lg lim ln log max min sup' |
| + ' limsup liminf injlim projlim Pr').split(' '); |
| for (var i = 0; i < mostOps.length; i += 1) { |
| BuiltInOpNames[mostOps[i]] = AutoOpNames[mostOps[i]] = 1; |
| } |
| |
| var builtInTrigs = // why coth but not sech and csch, LaTeX? |
| 'sin cos tan arcsin arccos arctan sinh cosh tanh sec csc cot coth'.split(' '); |
| for (var i = 0; i < builtInTrigs.length; i += 1) { |
| BuiltInOpNames[builtInTrigs[i]] = 1; |
| } |
| |
| var autoTrigs = 'sin cos tan sec cosec csc cotan cot ctg'.split(' '); |
| for (var i = 0; i < autoTrigs.length; i += 1) { |
| AutoOpNames[autoTrigs[i]] = |
| AutoOpNames['arc'+autoTrigs[i]] = |
| AutoOpNames[autoTrigs[i]+'h'] = |
| AutoOpNames['ar'+autoTrigs[i]+'h'] = |
| AutoOpNames['arc'+autoTrigs[i]+'h'] = 1; |
| } |
| |
| // compat with some of the nonstandard LaTeX exported by MathQuill |
| // before #247. None of these are real LaTeX commands so, seems safe |
| var moreNonstandardOps = 'gcf hcf lcm proj span'.split(' '); |
| for (var i = 0; i < moreNonstandardOps.length; i += 1) { |
| AutoOpNames[moreNonstandardOps[i]] = 1; |
| } |
| }()); |
| optionProcessors.autoOperatorNames = function(cmds) { |
| if (!/^[a-z]+(?: [a-z]+)*$/i.test(cmds)) { |
| throw '"'+cmds+'" not a space-delimited list of only letters'; |
| } |
| var list = cmds.split(' '), dict = {}, maxLength = 0; |
| for (var i = 0; i < list.length; i += 1) { |
| var cmd = list[i]; |
| if (cmd.length < 2) { |
| throw '"'+cmd+'" not minimum length of 2'; |
| } |
| dict[cmd] = 1; |
| maxLength = max(maxLength, cmd.length); |
| } |
| dict._maxLength = maxLength; |
| return dict; |
| }; |
| var OperatorName = P(Symbol, function(_, super_) { |
| _.init = function(fn) { this.ctrlSeq = fn; }; |
| _.createLeftOf = function(cursor) { |
| var fn = this.ctrlSeq; |
| for (var i = 0; i < fn.length; i += 1) { |
| Letter(fn.charAt(i)).createLeftOf(cursor); |
| } |
| }; |
| _.parser = function() { |
| var fn = this.ctrlSeq; |
| var block = MathBlock(); |
| for (var i = 0; i < fn.length; i += 1) { |
| Letter(fn.charAt(i)).adopt(block, block.ends[R], 0); |
| } |
| return Parser.succeed(block.children()); |
| }; |
| }); |
| for (var fn in AutoOpNames) if (AutoOpNames.hasOwnProperty(fn)) { |
| LatexCmds[fn] = OperatorName; |
| } |
| LatexCmds.operatorname = P(MathCommand, function(_) { |
| _.createLeftOf = noop; |
| _.numBlocks = function() { return 1; }; |
| _.parser = function() { |
| return latexMathParser.block.map(function(b) { return b.children(); }); |
| }; |
| }); |
| |
| LatexCmds.f = P(Letter, function(_, super_) { |
| _.init = function() { |
| Symbol.p.init.call(this, this.letter = 'f', '<var class="mq-f">f</var>'); |
| }; |
| _.italicize = function(bool) { |
| this.jQ.html('f').toggleClass('mq-f', bool); |
| return super_.italicize.apply(this, arguments); |
| }; |
| }); |
| |
| // VanillaSymbol's |
| LatexCmds[' '] = LatexCmds.space = bind(VanillaSymbol, '\\ ', ' '); |
| |
| LatexCmds["'"] = LatexCmds.prime = bind(VanillaSymbol, "'", '′'); |
| LatexCmds['\u2033'] = LatexCmds.dprime = bind(VanillaSymbol, '\u2033', '″'); |
| |
| LatexCmds.backslash = bind(VanillaSymbol,'\\backslash ','\\'); |
| if (!CharCmds['\\']) CharCmds['\\'] = LatexCmds.backslash; |
| |
| LatexCmds.$ = bind(VanillaSymbol, '\\$', '$'); |
| |
| // does not use Symbola font |
| var NonSymbolaSymbol = P(Symbol, function(_, super_) { |
| _.init = function(ch, html) { |
| super_.init.call(this, ch, '<span class="mq-nonSymbola">'+(html || ch)+'</span>'); |
| }; |
| }); |
| |
| LatexCmds['@'] = NonSymbolaSymbol; |
| LatexCmds['&'] = bind(NonSymbolaSymbol, '\\&', '&'); |
| LatexCmds['%'] = bind(NonSymbolaSymbol, '\\%', '%'); |
| |
| //the following are all Greek to me, but this helped a lot: http://www.ams.org/STIX/ion/stixsig03.html |
| |
| //lowercase Greek letter variables |
| LatexCmds.alpha = |
| LatexCmds.beta = |
| LatexCmds.gamma = |
| LatexCmds.delta = |
| LatexCmds.zeta = |
| LatexCmds.eta = |
| LatexCmds.theta = |
| LatexCmds.iota = |
| LatexCmds.kappa = |
| LatexCmds.mu = |
| LatexCmds.nu = |
| LatexCmds.xi = |
| LatexCmds.rho = |
| LatexCmds.sigma = |
| LatexCmds.tau = |
| LatexCmds.chi = |
| LatexCmds.psi = |
| LatexCmds.omega = P(Variable, function(_, super_) { |
| _.init = function(latex) { |
| super_.init.call(this,'\\'+latex+' ','&'+latex+';'); |
| }; |
| }); |
| |
| //why can't anybody FUCKING agree on these |
| LatexCmds.phi = //W3C or Unicode? |
| bind(Variable,'\\phi ','ϕ'); |
| |
| LatexCmds.phiv = //Elsevier and 9573-13 |
| LatexCmds.varphi = //AMS and LaTeX |
| bind(Variable,'\\varphi ','φ'); |
| |
| LatexCmds.epsilon = //W3C or Unicode? |
| bind(Variable,'\\epsilon ','ϵ'); |
| |
| LatexCmds.epsiv = //Elsevier and 9573-13 |
| LatexCmds.varepsilon = //AMS and LaTeX |
| bind(Variable,'\\varepsilon ','ε'); |
| |
| LatexCmds.piv = //W3C/Unicode and Elsevier and 9573-13 |
| LatexCmds.varpi = //AMS and LaTeX |
| bind(Variable,'\\varpi ','ϖ'); |
| |
| LatexCmds.sigmaf = //W3C/Unicode |
| LatexCmds.sigmav = //Elsevier |
| LatexCmds.varsigma = //LaTeX |
| bind(Variable,'\\varsigma ','ς'); |
| |
| LatexCmds.thetav = //Elsevier and 9573-13 |
| LatexCmds.vartheta = //AMS and LaTeX |
| LatexCmds.thetasym = //W3C/Unicode |
| bind(Variable,'\\vartheta ','ϑ'); |
| |
| LatexCmds.upsilon = //AMS and LaTeX and W3C/Unicode |
| LatexCmds.upsi = //Elsevier and 9573-13 |
| bind(Variable,'\\upsilon ','υ'); |
| |
| //these aren't even mentioned in the HTML character entity references |
| LatexCmds.gammad = //Elsevier |
| LatexCmds.Gammad = //9573-13 -- WTF, right? I dunno if this was a typo in the reference (see above) |
| LatexCmds.digamma = //LaTeX |
| bind(Variable,'\\digamma ','ϝ'); |
| |
| LatexCmds.kappav = //Elsevier |
| LatexCmds.varkappa = //AMS and LaTeX |
| bind(Variable,'\\varkappa ','ϰ'); |
| |
| LatexCmds.rhov = //Elsevier and 9573-13 |
| LatexCmds.varrho = //AMS and LaTeX |
| bind(Variable,'\\varrho ','ϱ'); |
| |
| //Greek constants, look best in non-italicized Times New Roman |
| LatexCmds.pi = LatexCmds['\u03c0'] = bind(NonSymbolaSymbol,'\\pi ','π'); |
| LatexCmds.lambda = bind(NonSymbolaSymbol,'\\lambda ','λ'); |
| |
| //uppercase greek letters |
| |
| LatexCmds.Upsilon = //LaTeX |
| LatexCmds.Upsi = //Elsevier and 9573-13 |
| LatexCmds.upsih = //W3C/Unicode "upsilon with hook" |
| LatexCmds.Upsih = //'cos it makes sense to me |
| bind(Symbol,'\\Upsilon ','<var style="font-family: serif">ϒ</var>'); //Symbola's 'upsilon with a hook' is a capital Y without hooks :( |
| |
| //other symbols with the same LaTeX command and HTML character entity reference |
| LatexCmds.Gamma = |
| LatexCmds.Delta = |
| LatexCmds.Theta = |
| LatexCmds.Lambda = |
| LatexCmds.Xi = |
| LatexCmds.Pi = |
| LatexCmds.Sigma = |
| LatexCmds.Phi = |
| LatexCmds.Psi = |
| LatexCmds.Omega = |
| LatexCmds.forall = P(VanillaSymbol, function(_, super_) { |
| _.init = function(latex) { |
| super_.init.call(this,'\\'+latex+' ','&'+latex+';'); |
| }; |
| }); |
| |
| // symbols that aren't a single MathCommand, but are instead a whole |
| // Fragment. Creates the Fragment from a LaTeX string |
| var LatexFragment = P(MathCommand, function(_) { |
| _.init = function(latex) { this.latex = latex; }; |
| _.createLeftOf = function(cursor) { |
| var block = latexMathParser.parse(this.latex); |
| block.children().adopt(cursor.parent, cursor[L], cursor[R]); |
| cursor[L] = block.ends[R]; |
| block.jQize().insertBefore(cursor.jQ); |
| block.finalizeInsert(cursor.options, cursor); |
| if (block.ends[R][R].siblingCreated) block.ends[R][R].siblingCreated(cursor.options, L); |
| if (block.ends[L][L].siblingCreated) block.ends[L][L].siblingCreated(cursor.options, R); |
| cursor.parent.bubble('reflow'); |
| }; |
| _.parser = function() { |
| var frag = latexMathParser.parse(this.latex).children(); |
| return Parser.succeed(frag); |
| }; |
| }); |
| |
| // for what seems to me like [stupid reasons][1], Unicode provides |
| // subscripted and superscripted versions of all ten Arabic numerals, |
| // as well as [so-called "vulgar fractions"][2]. |
| // Nobody really cares about most of them, but some of them actually |
| // predate Unicode, dating back to [ISO-8859-1][3], apparently also |
| // known as "Latin-1", which among other things [Windows-1252][4] |
| // largely coincides with, so Microsoft Word sometimes inserts them |
| // and they get copy-pasted into MathQuill. |
| // |
| // (Irrelevant but funny story: though not a superset of Latin-1 aka |
| // ISO-8859-1, Windows-1252 **is** a strict superset of the "closely |
| // related but distinct"[3] "ISO 8859-1" -- see the lack of a dash |
| // after "ISO"? Completely different character set, like elephants vs |
| // elephant seals, or "Zombies" vs "Zombie Redneck Torture Family". |
| // What kind of idiot would get them confused. |
| // People in fact got them confused so much, it was so common to |
| // mislabel Windows-1252 text as ISO-8859-1, that most modern web |
| // browsers and email clients treat the MIME charset of ISO-8859-1 |
| // as actually Windows-1252, behavior now standard in the HTML5 spec.) |
| // |
| // [1]: http://en.wikipedia.org/wiki/Unicode_subscripts_andsuper_scripts |
| // [2]: http://en.wikipedia.org/wiki/Number_Forms |
| // [3]: http://en.wikipedia.org/wiki/ISO/IEC_8859-1 |
| // [4]: http://en.wikipedia.org/wiki/Windows-1252 |
| LatexCmds['\u00b9'] = bind(LatexFragment, '^1'); |
| LatexCmds['\u00b2'] = bind(LatexFragment, '^2'); |
| LatexCmds['\u00b3'] = bind(LatexFragment, '^3'); |
| LatexCmds['\u00bc'] = bind(LatexFragment, '\\frac14'); |
| LatexCmds['\u00bd'] = bind(LatexFragment, '\\frac12'); |
| LatexCmds['\u00be'] = bind(LatexFragment, '\\frac34'); |
| |
| var PlusMinus = P(BinaryOperator, function(_) { |
| _.init = VanillaSymbol.prototype.init; |
| |
| _.contactWeld = _.siblingCreated = _.siblingDeleted = function(opts, dir) { |
| function determineOpClassType(node) { |
| if (node[L]) { |
| // If the left sibling is a binary operator or a separator (comma, semicolon, colon) |
| // or an open bracket (open parenthesis, open square bracket) |
| // consider the operator to be unary |
| if (node[L] instanceof BinaryOperator || /^[,;:\(\[]$/.test(node[L].ctrlSeq)) { |
| return ''; |
| } |
| } else if (node.parent && node.parent.parent && node.parent.parent.isStyleBlock()) { |
| //if we are in a style block at the leftmost edge, determine unary/binary based on |
| //the style block |
| //this allows style blocks to be transparent for unary/binary purposes |
| return determineOpClassType(node.parent.parent); |
| } else { |
| return ''; |
| } |
| |
| return 'mq-binary-operator'; |
| }; |
| |
| if (dir === R) return; // ignore if sibling only changed on the right |
| this.jQ[0].className = determineOpClassType(this); |
| return this; |
| }; |
| }); |
| |
| LatexCmds['+'] = bind(PlusMinus, '+', '+'); |
| //yes, these are different dashes, I think one is an en dash and the other is a hyphen |
| LatexCmds['\u2013'] = LatexCmds['-'] = bind(PlusMinus, '-', '−'); |
| LatexCmds['\u00b1'] = LatexCmds.pm = LatexCmds.plusmn = LatexCmds.plusminus = |
| bind(PlusMinus,'\\pm ','±'); |
| LatexCmds.mp = LatexCmds.mnplus = LatexCmds.minusplus = |
| bind(PlusMinus,'\\mp ','∓'); |
| |
| CharCmds['*'] = LatexCmds.sdot = LatexCmds.cdot = |
| bind(BinaryOperator, '\\cdot ', '·', '*'); |
| //semantically should be ⋅, but · looks better |
| |
| var Inequality = P(BinaryOperator, function(_, super_) { |
| _.init = function(data, strict) { |
| this.data = data; |
| this.strict = strict; |
| var strictness = (strict ? 'Strict' : ''); |
| super_.init.call(this, data['ctrlSeq'+strictness], data['html'+strictness], |
| data['text'+strictness]); |
| }; |
| _.swap = function(strict) { |
| this.strict = strict; |
| var strictness = (strict ? 'Strict' : ''); |
| this.ctrlSeq = this.data['ctrlSeq'+strictness]; |
| this.jQ.html(this.data['html'+strictness]); |
| this.textTemplate = [ this.data['text'+strictness] ]; |
| }; |
| _.deleteTowards = function(dir, cursor) { |
| if (dir === L && !this.strict) { |
| this.swap(true); |
| this.bubble('reflow'); |
| return; |
| } |
| super_.deleteTowards.apply(this, arguments); |
| }; |
| }); |
| |
| var less = { ctrlSeq: '\\le ', html: '≤', text: '\u2264', |
| ctrlSeqStrict: '<', htmlStrict: '<', textStrict: '<' }; |
| var greater = { ctrlSeq: '\\ge ', html: '≥', text: '\u2265', |
| ctrlSeqStrict: '>', htmlStrict: '>', textStrict: '>' }; |
| |
| LatexCmds['<'] = LatexCmds.lt = bind(Inequality, less, true); |
| LatexCmds['>'] = LatexCmds.gt = bind(Inequality, greater, true); |
| LatexCmds['\u2264'] = LatexCmds.le = LatexCmds.leq = bind(Inequality, less, false); |
| LatexCmds['\u2265'] = LatexCmds.ge = LatexCmds.geq = bind(Inequality, greater, false); |
| |
| var Equality = P(BinaryOperator, function(_, super_) { |
| _.init = function() { |
| super_.init.call(this, '=', '='); |
| }; |
| _.createLeftOf = function(cursor) { |
| if (cursor[L] instanceof Inequality && cursor[L].strict) { |
| cursor[L].swap(false); |
| cursor[L].bubble('reflow'); |
| return; |
| } |
| super_.createLeftOf.apply(this, arguments); |
| }; |
| }); |
| LatexCmds['='] = Equality; |
| |
| LatexCmds['\u00d7'] = LatexCmds.times = bind(BinaryOperator, '\\times ', '×', '[x]'); |
| |
| LatexCmds['\u00f7'] = LatexCmds.div = LatexCmds.divide = LatexCmds.divides = |
| bind(BinaryOperator,'\\div ','÷', '[/]'); |
| |
| CharCmds['~'] = LatexCmds.sim = bind(BinaryOperator, '\\sim ', '~', '~'); |
| var MQ1 = getInterface(1); |
| for (var key in MQ1) (function(key, val) { |
| if (typeof val === 'function') { |
| MathQuill[key] = function() { |
| insistOnInterVer(); |
| return val.apply(this, arguments); |
| }; |
| MathQuill[key].prototype = val.prototype; |
| } |
| else MathQuill[key] = val; |
| }(key, MQ1[key])); |
| |
| }()); |