blob: a8d8d6fa6833019c31bdb78100ab728980230111 [file] [log] [blame]
/*************************************************
* 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 '&amp;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['÷'](ch);
else if (options && options.typingAsteriskWritesTimesSymbol && ch === '*')
return LatexCmds['×'](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;
};
});
};