| /************************************************* | |
| * 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) { | |
| 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 (cons = CharCmds[ch] || LatexCmds[ch]) | |
| return cons(ch); | |
| else | |
| return VanillaSymbol(ch); | |
| }; | |
| _.write = function(cursor, ch) { | |
| var cmd = this.chToCmd(ch); | |
| 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() { | |
| 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; | |
| }; | |
| }); | |
| }; |