diff --git a/Makefile b/Makefile index 46f811553..38851776c 100644 --- a/Makefile +++ b/Makefile @@ -24,36 +24,38 @@ SRC_DIR = ./src INTRO = $(SRC_DIR)/intro.js OUTRO = $(SRC_DIR)/outro.js -PJS_SRC = ./node_modules/pjs/src/p.js - BASE_SOURCES = \ - $(PJS_SRC) \ - $(SRC_DIR)/tree.js \ - $(SRC_DIR)/cursor.js \ - $(SRC_DIR)/controller.js \ - $(SRC_DIR)/publicapi.js \ - $(SRC_DIR)/services/parser.util.js \ - $(SRC_DIR)/services/saneKeyboardEvents.util.js \ - $(SRC_DIR)/services/aria.js \ - $(SRC_DIR)/services/exportText.js \ - $(SRC_DIR)/services/focusBlur.js \ - $(SRC_DIR)/services/keystroke.js \ - $(SRC_DIR)/services/latex.js \ - $(SRC_DIR)/services/mouse.js \ - $(SRC_DIR)/services/scrollHoriz.js \ - $(SRC_DIR)/services/textarea.js + $(SRC_DIR)/utils.ts \ + $(SRC_DIR)/services/aria.ts \ + $(SRC_DIR)/tree.ts \ + $(SRC_DIR)/cursor.ts \ + $(SRC_DIR)/controller.ts \ + $(SRC_DIR)/publicapi.ts \ + $(SRC_DIR)/services/parser.util.ts \ + $(SRC_DIR)/services/saneKeyboardEvents.util.ts \ + $(SRC_DIR)/services/exportText.ts \ + $(SRC_DIR)/services/focusBlur.ts \ + $(SRC_DIR)/services/keystroke.ts \ + $(SRC_DIR)/services/latex.ts \ + $(SRC_DIR)/services/mouse.ts \ + $(SRC_DIR)/services/scrollHoriz.ts \ + $(SRC_DIR)/services/textarea.ts SOURCES_FULL = \ $(BASE_SOURCES) \ - $(SRC_DIR)/commands/math.js \ - $(SRC_DIR)/commands/text.js \ - $(SRC_DIR)/commands/math/*.js + $(SRC_DIR)/commands/math.ts \ + $(SRC_DIR)/commands/text.ts \ + $(SRC_DIR)/commands/math/advancedSymbols.ts \ + $(SRC_DIR)/commands/math/basicSymbols.ts \ + $(SRC_DIR)/commands/math/commands.ts \ + $(SRC_DIR)/commands/math/LatexCommandInput.ts + SOURCES_BASIC = \ $(BASE_SOURCES) \ - $(SRC_DIR)/commands/math.js \ - $(SRC_DIR)/commands/math/basicSymbols.js \ - $(SRC_DIR)/commands/math/commands.js + $(SRC_DIR)/commands/math.ts \ + $(SRC_DIR)/commands/math/basicSymbols.ts \ + $(SRC_DIR)/commands/math/commands.ts CSS_DIR = $(SRC_DIR)/css CSS_MAIN = $(CSS_DIR)/main.less @@ -113,10 +115,8 @@ font: $(FONT_TARGET) clean: rm -rf $(BUILD_DIR) -$(PJS_SRC): $(NODE_MODULES_INSTALLED) - $(BUILD_JS): $(INTRO) $(SOURCES_FULL) $(OUTRO) $(BUILD_DIR_EXISTS) - cat $^ | ./script/escape-non-ascii > $@ + cat $^ | ./script/escape-non-ascii | ./script/tsc-emit-only > $@ perl -pi -e s/mq-/$(MQ_CLASS_PREFIX)mq-/g $@ perl -pi -e s/{VERSION}/v$(VERSION)/ $@ @@ -124,7 +124,7 @@ $(UGLY_JS): $(BUILD_JS) $(NODE_MODULES_INSTALLED) $(UGLIFY) $(UGLIFY_OPTS) < $< > $@ $(BASIC_JS): $(INTRO) $(SOURCES_BASIC) $(OUTRO) $(BUILD_DIR_EXISTS) - cat $^ | ./script/escape-non-ascii > $@ + cat $^ | ./script/escape-non-ascii | ./script/tsc-emit-only > $@ perl -pi -e s/mq-/$(MQ_CLASS_PREFIX)mq-/g $@ perl -pi -e s/{VERSION}/v$(VERSION)/ $@ @@ -157,14 +157,20 @@ $(FONT_TARGET): $(FONT_SOURCE) $(BUILD_DIR_EXISTS) # # -*- Test tasks -*- # +.PHONY: +lint: + npx tsc --noEmit -.PHONY: test server run-server +.PHONY: test server benchmark server: node script/test_server.js test: dev $(BUILD_TEST) $(BASIC_JS) $(BASIC_CSS) @echo @echo "** now open test/{unit,visual}.html in your browser to run the {unit,visual} tests. **" +benchmark: dev $(BUILD_TEST) $(BASIC_JS) $(BASIC_CSS) + @echo + @echo "** now open benchmark/select.html in your browser. **" $(BUILD_TEST): $(INTRO) $(SOURCES_FULL) $(UNIT_TESTS) $(OUTRO) $(BUILD_DIR_EXISTS) - cat $^ > $@ + cat $^ | ./script/tsc-emit-only > $@ perl -pi -e s/{VERSION}/v$(VERSION)/ $@ diff --git a/benchmark/select.html b/benchmark/select.html new file mode 100644 index 000000000..e77904d66 --- /dev/null +++ b/benchmark/select.html @@ -0,0 +1,77 @@ + + + + + + + +MathQuill Select benchmark + + + + + + +

Benchmark inserting and then selecting n characters

+ +
+ +
+ + + + + + + + +
ncharsrender (ms)select (ms)
+ + + + + diff --git a/docs/Config.md b/docs/Config.md index b8125ef49..5f2736e12 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -101,24 +101,24 @@ For example, [Desmos](https://www.desmos.com/calculator) substitutes `')[0], { handlers: this }); $(math.el()).appendTo(this.el); math.data.i = this.maths.length; this.maths.push(math); }; - _.moveOutOf = function(dir, math) { + moveOutOf (dir, math) { var adjacentI = (dir === MQ.L ? math.data.i - 1 : math.data.i + 1); var adjacentMath = this.maths[adjacentI]; if (adjacentMath) adjacentMath.focus().moveToDirEnd(-dir); }; ... -}); +}; ``` It's common to just ignore the last argument, like if the handlers close over the math field: diff --git a/package-lock.json b/package-lock.json index 489f69df9..6c41440cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -651,12 +651,6 @@ "dev": true, "optional": true }, - "pjs": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pjs/-/pjs-4.0.0.tgz", - "integrity": "sha1-aMp9me0z1KZSuLe0P5lvOR71Efk=", - "dev": true - }, "promise": { "version": "7.3.1", "resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz", @@ -826,6 +820,12 @@ "dev": true, "optional": true }, + "typescript": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.2.tgz", + "integrity": "sha512-5BlMof9H1yGt0P8/WF+wPNw6GfctgGjXp5hkblpyT+8rkASSmkUKMXrxR0Xg8ThVCi/JnHQiKXeBaEwCeQwMFw==", + "dev": true + }, "uglify-js": { "version": "2.8.29", "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", diff --git a/package.json b/package.json index 475a7c727..62ef80687 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "devDependencies": { "less": ">=1.5.1 <3.0.0", "mocha": ">=2.4.1", - "pjs": ">=3.1.0 <5.0.0", + "typescript": "^4.5.2", "uglify-js": "2.x" } } diff --git a/script/tsc-emit-only b/script/tsc-emit-only new file mode 100755 index 000000000..527ec982d --- /dev/null +++ b/script/tsc-emit-only @@ -0,0 +1,27 @@ +#!/usr/bin/env node + +var ts = require('typescript'); + +function compileTypescript (tsSource) { + compilerOptions = { + target: 'es5' + }; + + var jsSource = ts.transpileModule(tsSource, { + compilerOptions: compilerOptions + }).outputText; + + return jsSource; +} + +let contents = ''; + +process.stdin.setEncoding('utf8'); +process.stdin.on('data', function(data) { + contents += data; +}); +process.stdin.on('end', function () { + var ts = compileTypescript(contents); + console.log(ts); +}) +process.stdin.resume(); diff --git a/src/commands/math.js b/src/commands/math.ts similarity index 59% rename from src/commands/math.js rename to src/commands/math.ts index 48bfd0e8d..24dea4b82 100644 --- a/src/commands/math.js +++ b/src/commands/math.ts @@ -4,13 +4,11 @@ /** * Math tree node base class. - * Some math-tree-specific extensions to Node. + * Some math-tree-specific extensions to MQNode. * 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 +class MathElement extends MQNode { + finalizeInsert (options:CursorOptions, cursor:Cursor) { var self = this; self.postOrder(function (node) { node.finalizeTree(options) }); self.postOrder(function (node) { node.contactWeld(cursor) }); @@ -19,17 +17,22 @@ var MathElement = P(Node, function(_, super_) { // 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(function (node) { node.blur(); }); + self.postOrder(function (node) { node.blur(cursor); }); self.postOrder(function (node) { node.reflow(); }); - if (self[R].siblingCreated) self[R].siblingCreated(options, L); - if (self[L].siblingCreated) self[L].siblingCreated(options, R); - self.bubble(function (node) { node.reflow(); }); + var selfR = self[R]; + var selfL = self[L]; + if (selfR) selfR.siblingCreated(options, L); + if (selfL) selfL.siblingCreated(options, R); + self.bubble(function (node) { + node.reflow(); + return undefined; + }); }; // If the maxDepth option is set, make sure // deeply nested content is truncated. Just return // false if the cursor is already too deep. - _.prepareInsertionAt = function(cursor) { + prepareInsertionAt (cursor:Cursor) { var maxDepth = cursor.options.maxDepth; if (maxDepth !== undefined) { var cursorDepth = cursor.depth(); @@ -42,99 +45,104 @@ var MathElement = P(Node, function(_, super_) { }; // Remove nodes that are more than `cutoff` // blocks deep from this node. - _.removeNodesDeeperThan = function (cutoff) { + removeNodesDeeperThan (cutoff:number) { var depth = 0; - var queue = [[this, depth]]; - var current; + var queue:[[MQNode, number]] = [[this, depth]]; + var current:[MQNode, number] | undefined; // Do a breadth-first search of this node's descendants // down to cutoff, removing anything deeper. - while (queue.length) { - current = queue.shift(); - current[0].children().each(function (child) { + while (current = queue.shift()) { + var c = current; + c[0].children().each(function (child) { var i = (child instanceof MathBlock) ? 1 : 0; - depth = current[1]+i; + depth = c[1]+i; if (depth <= cutoff) { queue.push([child, depth]); } else { (i ? child.children() : child).remove(); } + return undefined; }); } }; -}); +} /** * 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); +class MathCommand extends MathElement { + replacedFragment:Fragment | undefined; - if (!cmd.ctrlSeq) cmd.ctrlSeq = ctrlSeq; - if (htmlTemplate) cmd.htmlTemplate = htmlTemplate; - if (textTemplate) cmd.textTemplate = textTemplate; - }; + constructor (ctrlSeq?:string, htmlTemplate?:string, textTemplate?:string[]) { + super(); + this.setCtrlSeqHtmlAndText(ctrlSeq, htmlTemplate, textTemplate); + } + + setCtrlSeqHtmlAndText (ctrlSeq?:string, htmlTemplate?:string, textTemplate?:string[]) { + if (!this.ctrlSeq) this.ctrlSeq = ctrlSeq; + if (htmlTemplate) this.htmlTemplate = htmlTemplate; + if (textTemplate) this.textTemplate = textTemplate; + } // obvious methods - _.replaces = function(replacedFragment) { + replaces (replacedFragment:Fragment) { replacedFragment.disown(); this.replacedFragment = replacedFragment; }; - _.isEmpty = function() { + isEmpty () { return this.foldChildren(true, function(isEmpty, child) { return isEmpty && child.isEmpty(); }); }; - _.parser = function() { + parser ():Parser { var block = latexMathParser.block; - var self = this; - return block.times(self.numBlocks()).map(function(blocks) { - self.blocks = blocks; + return block.times(this.numBlocks()).map((blocks) => { + this.blocks = blocks; for (var i = 0; i < blocks.length; i += 1) { - blocks[i].adopt(self, self.ends[R], 0); + blocks[i].adopt(this, this.ends[R], 0); } - return self; + return this; }); }; // createLeftOf(cursor) and the methods it calls - _.createLeftOf = function(cursor) { + createLeftOf (cursor:Cursor) { var cmd = this; var replacedFragment = cmd.replacedFragment; cmd.createBlocks(); - super_.createLeftOf.call(cmd, cursor); + super.createLeftOf(cursor); if (replacedFragment) { - replacedFragment.adopt(cmd.ends[L], 0, 0); - replacedFragment.jQ.appendTo(cmd.ends[L].jQ); + const cmdEndsL = cmd.ends[L] as MQNode; + replacedFragment.adopt(cmdEndsL, 0, 0); + replacedFragment.jQ.appendTo(cmdEndsL.jQ); cmd.placeCursor(cursor); cmd.prepareInsertionAt(cursor); } - cmd.finalizeInsert(cursor.options); + cmd.finalizeInsert(cursor.options, cursor); cmd.placeCursor(cursor); }; - _.createBlocks = function() { + createBlocks () { var cmd = this, numBlocks = cmd.numBlocks(), blocks = cmd.blocks = Array(numBlocks); for (var i = 0; i < numBlocks; i += 1) { - var newBlock = blocks[i] = MathBlock(); + var newBlock = blocks[i] = new MathBlock(); newBlock.adopt(cmd, cmd.ends[R], 0); } }; - _.placeCursor = function(cursor) { + placeCursor (cursor: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) { + cursor.insAtRightEnd(this.foldChildren(this.ends[L] as MQNode, function(leftward, child) { return leftward.isEmpty() ? leftward : child; })); }; @@ -142,31 +150,42 @@ var MathCommand = P(MathElement, function(_, super_) { // 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]); - aria.queueDirEndOf(-dir).queue(cursor.parent, true); + moveTowards (dir:Direction, cursor:Cursor, updown?:'up'|'down') { + var updownInto:NodeRef | undefined; + if (updown === 'up') { + updownInto = this.upInto; + } else if (updown === 'down') { + updownInto = this.downInto; + } + + const el = (updownInto || this.ends[-dir as Direction]) as MQNode; + cursor.insAtDirEnd(-dir as Direction, el); + cursor.controller.aria.queueDirEndOf(-dir as Direction).queue(cursor.parent, true); }; - _.deleteTowards = function(dir, cursor) { + deleteTowards (dir:Direction, cursor:Cursor) { if (this.isEmpty()) cursor[dir] = this.remove()[dir]; - else this.moveTowards(dir, cursor, null); + else this.moveTowards(dir, cursor); }; - _.selectTowards = function(dir, cursor) { - cursor[-dir] = this; + selectTowards (dir:Direction, cursor:Cursor) { + cursor[-dir as Direction] = 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; + selectChildren ():MQSelection { + return new MQSelection(this, this); + }; + unselectInto (dir:Direction, cursor:Cursor) { + const antiCursor = cursor.anticursor as Anticursor; + const ancestor = antiCursor.ancestors[this.id] as MQNode; + cursor.insAtDirEnd(-dir as Direction, ancestor); + }; + seek (pageX:number, cursor:Cursor) { + function getBounds(node:MQNode) { + var l:number = node.jQ.offset().left; + var r:number = l + node.jQ.outerWidth(); + return { + [L]: l, + [R]: r + } } var cmd = this; @@ -181,7 +200,7 @@ var MathCommand = P(MathElement, function(_, super_) { 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]); + if (block[L]) cursor.insAtRightEnd(block[L] as MQNode); else cursor.insLeftOf(cmd); } else cursor.insAtLeftEnd(block); @@ -196,12 +215,15 @@ var MathCommand = P(MathElement, function(_, super_) { } else cursor.insAtRightEnd(block); } + return undefined; } else { block.seek(pageX, cursor); return false; } }); + + return undefined; } // methods involved in creating and cross-linking with HTML DOM nodes @@ -230,11 +252,11 @@ var MathCommand = P(MathElement, function(_, super_) { Note that & 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); + numBlocks () { + var matches = (this.htmlTemplate as string).match(/&\d+/g); return matches ? matches.length : 0; }; - _.html = function() { + html () { // 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. @@ -271,9 +293,9 @@ var MathCommand = P(MathElement, function(_, super_) { // production without pray(), because it will then TypeError on .slice(). var cmd = this; - var blocks = cmd.blocks; + var blocks = cmd.blocks as MathBlock[]; var cmdId = ' mathquill-command-id=' + cmd.id; - var tokens = cmd.htmlTemplate.match(/<[^<>]+>|[^<>]+/g); + var tokens = (cmd.htmlTemplate as string).match(/<[^<>]+>|[^<>]+/g) as string[]; pray('no unmatched angle brackets', tokens.join('') === this.htmlTemplate); @@ -307,19 +329,20 @@ var MathCommand = P(MathElement, function(_, super_) { } while (nesting > 0); } } - return tokens.join('').replace(/>&(\d+)/g, function($0, $1) { - return ' mathquill-block-id=' + blocks[$1].id + ' aria-hidden="true">' + blocks[$1].join('html'); + return tokens.join('').replace(/>&(\d+)/g, function(_$0:string, $1:string) { + var num1 = parseInt($1, 10); + return ' mathquill-block-id=' + blocks[num1].id + ' aria-hidden="true">' + blocks[num1].join('html'); }); }; // methods to export a string representation of the math tree - _.latex = function() { - return this.foldChildren(this.ctrlSeq, function(latex, child) { + latex () { + return this.foldChildren(this.ctrlSeq || '', function(latex, child) { return latex + '{' + (child.latex() || ' ') + '}'; }); }; - _.textTemplate = ['']; - _.text = function() { + textTemplate = ['']; + text () { var cmd = this, i = 0; return cmd.foldChildren(cmd.textTemplate[i], function(text, child) { i += 1; @@ -330,100 +353,122 @@ var MathCommand = P(MathElement, function(_, super_) { return text + child_text + (cmd.textTemplate[i] || ''); }); }; - _.mathspeakTemplate = []; - _.mathspeak = function() { + mathspeakTemplate = ['']; + mathspeak () { var cmd = this, i = 0; return cmd.foldChildren(cmd.mathspeakTemplate[i] || 'Start'+cmd.ctrlSeq+' ', function(speech, block) { i += 1; return speech + ' ' + block.mathspeak() + ' ' + (cmd.mathspeakTemplate[i]+' ' || 'End'+cmd.ctrlSeq+' '); }); }; -}); +}; /** * Lightweight command without blocks or children. */ -var Symbol = P(MathCommand, function(_, super_) { - _.init = function(ctrlSeq, html, text, mathspeak) { - if (!text && !!ctrlSeq) text = ctrlSeq.replace(/^\\/, ''); +class MQSymbol extends MathCommand { + constructor (ctrlSeq?:string, html?:string, text?:string, mathspeak?:string) { + super(); + this.setCtrlSeqHtmlTextAndMathspeak(ctrlSeq, html, text, mathspeak); + }; + + setCtrlSeqHtmlTextAndMathspeak (ctrlSeq?:string, html?:string, text?:string, mathspeak?:string) { + if (!text && !!ctrlSeq) { + text = ctrlSeq.replace(/^\\/, ''); + } this.mathspeakName = mathspeak || text; - super_.init.call(this, ctrlSeq, html, [ text ]); - }; + super.setCtrlSeqHtmlAndText(ctrlSeq, html, [text || '']); + } - _.parser = function() { return Parser.succeed(this); }; - _.numBlocks = function() { return 0; }; + parser () { return Parser.succeed(this); }; + numBlocks () { return 0; }; - _.replaces = function(replacedFragment) { + replaces (replacedFragment:Fragment) { replacedFragment.remove(); }; - _.createBlocks = noop; + createBlocks () {}; - _.moveTowards = function(dir, cursor) { + moveTowards (dir:Direction, cursor:Cursor) { cursor.jQ.insDirOf(dir, this.jQ); - cursor[-dir] = this; + cursor[-dir as Direction] = this; cursor[dir] = this[dir]; - aria.queue(this); + cursor.controller.aria.queue(this); }; - _.deleteTowards = function(dir, cursor) { + deleteTowards (dir:Direction, cursor:Cursor) { cursor[dir] = this.remove()[dir]; }; - _.seek = function(pageX, cursor) { + seek (pageX:number, cursor: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.join(''); }; - _.mathspeak = function(){ return this.mathspeakName; }; - _.placeCursor = noop; - _.isEmpty = function(){ return true; }; -}); -var VanillaSymbol = P(Symbol, function(_, super_) { - _.init = function(ch, html, mathspeak) { - super_.init.call(this, ch, ''+(html || ch)+'', undefined, mathspeak); + return cursor; }; -}); -var BinaryOperator = P(Symbol, function(_, super_) { - _.init = function(ctrlSeq, html, text, mathspeak) { - super_.init.call(this, - ctrlSeq, ''+html+'', text, mathspeak - ); + + latex (){ return this.ctrlSeq || ''; }; + text (){ return this.textTemplate.join(''); }; + mathspeak (_opts?:MathspeakOptions){ return this.mathspeakName || ''; }; + placeCursor () {}; + isEmpty (){ return true; }; +}; +class VanillaSymbol extends MQSymbol { + constructor (ch:string, html?:string, mathspeak?:string) { + super(ch, ''+(html || ch)+'', undefined, mathspeak); + }; +} +function bindVanillaSymbol (ch:string, html?:string, mathspeak?:string) { + return () => new VanillaSymbol(ch, html, mathspeak); +} + +class BinaryOperator extends MQSymbol { + constructor (ctrlSeq?:string, html?:string, text?:string, mathspeak?:string, treatLikeSymbol?:boolean) { + if (treatLikeSymbol) { + super(ctrlSeq, ''+(html || ctrlSeq)+'', undefined, mathspeak); + } else { + super(ctrlSeq, ''+html+'', text, mathspeak); + } }; -}); +}; +function bindBinaryOperator (ctrlSeq?:string, html?:string, text?:string, mathspeak?:string) { + return () => new BinaryOperator(ctrlSeq, html, text, mathspeak); +} /** * 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) { +class MathBlock extends MathElement { + controller?:Controller; + + join (methodName:JoinMethod) { 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() : + html () { return this.join('html'); }; + latex () { return this.join('latex'); }; + text () { + var endsL = this.ends[L]; + var endsR = this.ends[R]; + return (endsL === endsR && endsL !== 0) ? + endsL.text() : this.join('text') ; }; - _.mathspeak = function() { + mathspeak () { var tempOp = ''; - var autoOps = {}; + var autoOps:CursorOptions['autoOperatorNames'] = {}; if (this.controller) autoOps = this.controller.options.autoOperatorNames; - return this.foldChildren([], function(speechArray, cmd) { + return this.foldChildren([], function(speechArray, cmd) { if (cmd.isPartOfOperator) { tempOp += cmd.mathspeak(); } else { if(tempOp!=='') { - if(autoOps !== {} && autoOps._maxLength > 0) { + if(autoOps._maxLength! > 0) { var x = autoOps[tempOp.toLowerCase()]; if(typeof x === 'string') tempOp = x; } @@ -433,7 +478,7 @@ var MathBlock = P(MathElement, function(_, super_) { var mathspeakText = cmd.mathspeak(); var cmdText = cmd.ctrlSeq; if ( - isNaN(cmdText) && + isNaN(cmdText as any) && // TODO - revisit this to improve the isNumber() check cmdText !== '.' && (!cmd.parent || !cmd.parent.parent || !cmd.parent.parent.isTextBlock()) ) { @@ -448,147 +493,168 @@ var MathBlock = P(MathElement, function(_, super_) { // For Apple devices in particular, split out digits after a decimal point so they aren't read aloud as whole words. // Not doing so makes 123.456 potentially spoken as "one hundred twenty-three point four hundred fifty-six." // Instead, add spaces so it is spoken as "one hundred twenty-three point four five six." - .replace(/(\.)([0-9]+)/g, function(match, p1, p2) { + .replace(/(\.)([0-9]+)/g, function(_match, p1, p2) { return p1 + p2.split('').join(' ').trim(); }); }; - _.ariaLabel = 'block'; - _.keystroke = function(key, e, ctrlr) { + ariaLabel = 'block'; + + keystroke (key:string, e:KeyboardEvent, ctrlr:Controller) { 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); + return super.keystroke(key, e, ctrlr); }; // 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']; + moveOutOf (dir:Direction, cursor:Cursor, updown?:'up'|'down') { + var updownInto:NodeRef | undefined; + if (updown === 'up') { + updownInto = this.parent.upInto; + } else if (updown === 'down') { + updownInto = this.parent.downInto; + } + if (!updownInto && this[dir]) { - cursor.insAtDirEnd(-dir, this[dir]); - aria.queueDirEndOf(-dir).queue(cursor.parent, true); + const otherDir = -dir as Direction; + cursor.insAtDirEnd(otherDir, this[dir] as MQNode); + cursor.controller.aria.queueDirEndOf(otherDir).queue(cursor.parent, true); } else { cursor.insDirOf(dir, this.parent); - aria.queueDirOf(dir).queue(this.parent); + cursor.controller.aria.queueDirOf(dir).queue(this.parent); } }; - _.selectOutOf = function(dir, cursor) { + selectOutOf (dir:Direction, cursor:Cursor) { cursor.insDirOf(dir, this.parent); }; - _.deleteOutOf = function(dir, cursor) { + deleteOutOf (_dir:Direction, cursor:Cursor) { cursor.unwrapGramp(); }; - _.seek = function(pageX, cursor) { + seek (pageX:number, cursor: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]; + + var endsL = this.ends[L] as MQNode; + if (pageX < endsL.jQ.offset().left) return cursor.insAtLeftEnd(this); + while (pageX < node.jQ.offset().left) node = node[L] as MQNode; return node.seek(pageX, cursor); }; - _.chToCmd = function(ch, options) { + chToCmd (ch:string, options:CursorOptions) { var cons; // exclude f because it gets a dedicated command with more spacing if (ch.match(/^[a-eg-zA-Z]$/)) - return Letter(ch); + return new Letter(ch); else if (/^\d$/.test(ch)) - return Digit(ch); + return new Digit(ch); else if (options && options.typingSlashWritesDivisionSymbol && ch === '/') - return LatexCmds['÷'](ch); + return (LatexCmds as LatexCmdsSingleCharBuilder)['÷'](ch); else if (options && options.typingAsteriskWritesTimesSymbol && ch === '*') - return LatexCmds['×'](ch); + return (LatexCmds as LatexCmdsSingleCharBuilder)['×'](ch); else if (options && options.typingPercentWritesPercentOf && ch === '%') - return LatexCmds.percentof(ch); - else if (cons = CharCmds[ch] || LatexCmds[ch]) - return cons(ch); + return (LatexCmds as LatexCmdsSingleCharBuilder).percentof(ch); + else if (cons = (CharCmds as CharCmdsAny)[ch] || (LatexCmds as LatexCmdsAny)[ch]) { + if (cons.constructor) { + return new cons(ch); + } else { + return cons(ch); + } + } else - return VanillaSymbol(ch); + return new VanillaSymbol(ch); }; - _.write = function(cursor, ch) { + write (cursor:Cursor, ch:string) { var cmd = this.chToCmd(ch, cursor.options); if (cursor.selection) cmd.replaces(cursor.replaceSelection()); if (!cursor.isTooDeep()) { cmd.createLeftOf(cursor.show()); // special-case the slash so that fractions are voiced while typing if (ch === '/') { - aria.alert('over'); + cursor.controller.aria.alert('over'); } else { - aria.alert(cmd.mathspeak({ createdLeftOf: cursor })); + cursor.controller.aria.alert(cmd.mathspeak({ createdLeftOf: cursor })); } } }; - _.writeLatex = function(cursor, latex) { + writeLatex (cursor:Cursor, latex:string) { var all = Parser.all; var eof = Parser.eof; - var block = latexMathParser.skip(eof).or(all.result(false)).parse(latex); + var block = latexMathParser.skip(eof).or(all.result(false)).parse(latex); if (block && !block.isEmpty() && block.prepareInsertionAt(cursor)) { - block.children().adopt(cursor.parent, cursor[L], cursor[R]); + block.children().adopt(cursor.parent, cursor[L] as NodeRef, cursor[R] as NodeRef); // TODO - masking undefined. should be 0 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(function (node) { node.reflow(); }); + var blockEndsR = block.ends[R]; + var blockEndsL = block.ends[L]; + var blockEndsRR = (blockEndsR as MQNode)[R]; + var blockEndsLL = (blockEndsL as MQNode)[L]; + if (blockEndsRR) blockEndsRR.siblingCreated(cursor.options, L); + if (blockEndsLL) blockEndsLL.siblingCreated(cursor.options, R); + cursor.parent.bubble(function (node) { + node.reflow(); + return undefined; + }); } }; - _.focus = function() { + focus () { this.jQ.addClass('mq-hasCursor'); this.jQ.removeClass('mq-empty'); return this; }; - _.blur = function() { + blur (cursor:Cursor) { this.jQ.removeClass('mq-hasCursor'); if (this.isEmpty()) { this.jQ.addClass('mq-empty'); - if (this.isEmptyParens()) { - this.jQ.addClass('mq-empty-parens'); - } else if (this.isEmptySquareBrackets()) { - this.jQ.addClass('mq-empty-square-brackets'); + if (cursor && this.isQuietEmptyDelimiter(cursor.options.quietEmptyDelimiters)) { + this.jQ.addClass('mq-quiet-delimiter'); } } return this; }; -}); +} + +Options.prototype.mouseEvents = true; +API.StaticMath = function(APIClasses:APIClasses) { + return class StaticMath extends APIClasses.AbstractMathQuill { + static RootBlock = MathBlock; -Options.p.mouseEvents = true; -API.StaticMath = function(APIClasses) { - return P(APIClasses.AbstractMathQuill, function(_, super_) { - this.RootBlock = MathBlock; - _.__mathquillify = function(opts, interfaceVersion) { + __mathquillify (opts:CursorOptions, _interfaceVersion:number) { this.config(opts); - super_.__mathquillify.call(this, 'mq-math-mode'); + super.__mathquillify('mq-math-mode'); if (this.__options.mouseEvents) { this.__controller.delegateMouseEvents(); this.__controller.staticMathTextareaEvents(); } return this; }; - _.init = function() { - super_.init.apply(this, arguments); + constructor (el:MQNode) { + super(el); var innerFields = this.innerFields = []; - this.__controller.root.postOrder(function (node) { + this.__controller.root.postOrder(function (node:MQNode) { node.registerInnerField(innerFields, APIClasses.InnerMathField); }); }; - _.latex = function() { - var returned = super_.latex.apply(this, arguments); + latex () { + var returned = super.latex.apply(this, arguments); if (arguments.length > 0) { var innerFields = this.innerFields = []; - this.__controller.root.postOrder(function (node) { + this.__controller.root.postOrder(function (node:MQNode) { node.registerInnerField(innerFields, APIClasses.InnerMathField); }); // Force an ARIA label update to remain in sync with the new LaTeX value. @@ -596,43 +662,46 @@ API.StaticMath = function(APIClasses) { } return returned; }; - _.setAriaLabel = function(ariaLabel) { + setAriaLabel (ariaLabel:string) { this.__controller.setAriaLabel(ariaLabel); return this; }; - _.getAriaLabel = function () { + getAriaLabel () { return this.__controller.getAriaLabel(); }; - }); + }; }; -var RootMathBlock = P(MathBlock, RootBlockMixin); -API.MathField = function(APIClasses) { - return P(APIClasses.EditableField, function(_, super_) { - this.RootBlock = RootMathBlock; - _.__mathquillify = function(opts, interfaceVersion) { +class RootMathBlock extends MathBlock {} +RootBlockMixin(RootMathBlock.prototype); // adds methods to RootMathBlock + +API.MathField = function(APIClasses:APIClasses) { + return class MathField extends APIClasses.EditableField { + static RootBlock = RootMathBlock; + + __mathquillify (opts:CursorOptions, interfaceVersion:number) { this.config(opts); if (interfaceVersion > 1) this.__controller.root.reflow = noop; - super_.__mathquillify.call(this, 'mq-editable-field mq-math-mode'); + super.__mathquillify('mq-editable-field mq-math-mode'); delete this.__controller.root.reflow; return this; }; - }); + }; }; -API.InnerMathField = function(APIClasses) { - return P(APIClasses.MathField, function(_, super_) { - _.makeStatic = function() { +API.InnerMathField = function(APIClasses:APIClasses) { + return class extends APIClasses.MathField { + makeStatic () { this.__controller.editable = false; this.__controller.root.blur(); this.__controller.unbindEditablesEvents(); this.__controller.container.removeClass('mq-editable-field'); }; - _.makeEditable = function() { + makeEditable () { this.__controller.editable = true; this.__controller.editablesTextareaEvents(); this.__controller.cursor.insAtRightEnd(this.__controller.root); this.__controller.container.addClass('mq-editable-field'); }; - }); + }; }; diff --git a/src/commands/math/LatexCommandInput.js b/src/commands/math/LatexCommandInput.js deleted file mode 100644 index dca839ab7..000000000 --- a/src/commands/math/LatexCommandInput.js +++ /dev/null @@ -1,103 +0,0 @@ -/**************************************** - * 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 = '\\&0'; - _.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); - // TODO needs tests - aria.alert(ch); - } - else { - var cmd = this.parent.renderCommand(cursor); - // TODO needs tests - aria.queue(cmd.mathspeak({ createdLeftOf: cursor })); - if (ch !== '\\' || !this.isEmpty()) cursor.parent.write(cursor, ch); - else aria.alert(); - } - }; - this.ends[L].keystroke = function(key, e, ctrlr) { - if (key === 'Tab' || key === 'Enter' || key === 'Spacebar') { - var cmd = this.parent.renderCommand(ctrlr.cursor); - // TODO needs tests - aria.alert(cmd.mathspeak({ createdLeftOf: 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]; - 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(); - } - return cmd; - }; -}); - diff --git a/src/commands/math/LatexCommandInput.ts b/src/commands/math/LatexCommandInput.ts new file mode 100644 index 000000000..528d57743 --- /dev/null +++ b/src/commands/math/LatexCommandInput.ts @@ -0,0 +1,119 @@ +/**************************************** + * Input box to type backslash commands + ***************************************/ + +CharCmds['\\'] = class LatexCommandInput extends MathCommand { + ctrlSeq = '\\'; + _replacedFragment?:Fragment; + + replaces (replacedFragment:Fragment) { + this._replacedFragment = replacedFragment.disown(); + this.isEmpty = function() { return false; }; + }; + htmlTemplate = '\\&0'; + textTemplate = ['\\']; + createBlocks () { + super.createBlocks(); + const endsL = this.ends[L] as MQNode; + + endsL.focus = function() { + this.parent.jQ.addClass('mq-hasCursor'); + if (this.isEmpty()) + this.parent.jQ.removeClass('mq-empty'); + + return this; + }; + endsL.blur = function() { + this.parent.jQ.removeClass('mq-hasCursor'); + if (this.isEmpty()) + this.parent.jQ.addClass('mq-empty'); + + return this; + }; + endsL.write = function(cursor, ch) { + cursor.show().deleteSelection(); + + if (ch.match(/[a-z]/i)) { + new VanillaSymbol(ch).createLeftOf(cursor); + // TODO needs tests + cursor.controller.aria.alert(ch); + } + else { + var cmd = (this.parent as LatexCommandInput).renderCommand(cursor); + // TODO needs tests + cursor.controller.aria.queue(cmd.mathspeak({ createdLeftOf: cursor })); + if (ch !== '\\' || !this.isEmpty()) cursor.parent.write(cursor, ch); + else cursor.controller.aria.alert(); + } + }; + + var originalKeystroke = endsL.keystroke; + endsL.keystroke = function(key, e, ctrlr) { + if (key === 'Tab' || key === 'Enter' || key === 'Spacebar') { + var cmd = (this.parent as LatexCommandInput).renderCommand(ctrlr.cursor); + // TODO needs tests + ctrlr.aria.alert(cmd.mathspeak({ createdLeftOf: ctrlr.cursor })); + e.preventDefault(); + return; + } + + return originalKeystroke.call(this, key, e, ctrlr); + }; + }; + createLeftOf (cursor:Cursor) { + super.createLeftOf(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) { + // TODO - overwritting e.target + (e as any).target = el + $(el).trigger(e); + return false; + } + ).insertBefore(this.jQ).add(this.jQ); + } + }; + latex () { + return '\\' + (this.ends[L] as MQNode).latex() + ' '; + }; + renderCommand (cursor:Cursor) { + this.jQ = this.jQ.last(); + this.remove(); + if (this[R]) { + cursor.insLeftOf(this[R] as MQNode); + } else { + cursor.insAtRightEnd(this.parent); + } + + var latex = (this.ends[L] as MQNode).latex(); + if (!latex) latex = ' '; + var cmd = LatexCmds[latex]; + + if (cmd) { + let node:MQNode; + if (isMQNodeClass(cmd)) { + node = new (cmd as typeof TempSingleCharNode)(latex); + } else { + node = cmd(latex); + } + if (this._replacedFragment) (node as MathCommand).replaces(this._replacedFragment); + node.createLeftOf(cursor); + return node; + } + else { + const node = new TextBlock(); + node.replaces(latex); + node.createLeftOf(cursor); + cursor.insRightOf(node); + if (this._replacedFragment) { + this._replacedFragment.remove(); + } + return node; + } + }; +}; + diff --git a/src/commands/math/advancedSymbols.js b/src/commands/math/advancedSymbols.js deleted file mode 100644 index 865edfc9c..000000000 --- a/src/commands/math/advancedSymbols.js +++ /dev/null @@ -1,338 +0,0 @@ -/************************************ - * 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['∗'] = LatexCmds.ast = LatexCmds.star = LatexCmds.loast = LatexCmds.lowast = - bind(BinaryOperator,'\\ast ','∗', 'low asterisk'); -LatexCmds.therefor = LatexCmds.therefore = - bind(BinaryOperator,'\\therefore ','∴', 'therefore'); - -LatexCmds.cuz = // l33t -LatexCmds.because = bind(BinaryOperator,'\\because ','∵', 'because'); - -LatexCmds.prop = LatexCmds.propto = bind(BinaryOperator,'\\propto ','∝', 'proportional to'); - -LatexCmds['≈'] = LatexCmds.asymp = LatexCmds.approx = bind(BinaryOperator,'\\approx ','≈'), 'approximately equal to'; - -LatexCmds.isin = LatexCmds['in'] = bind(BinaryOperator,'\\in ','∈', 'is in'); - -LatexCmds.ni = LatexCmds.contains = bind(BinaryOperator,'\\ni ','∋', 'is not in'); - -LatexCmds.notni = LatexCmds.niton = LatexCmds.notcontains = LatexCmds.doesnotcontain = - bind(BinaryOperator,'\\not\\ni ','∌', 'does not contain'); - -LatexCmds.sub = LatexCmds.subset = bind(BinaryOperator,'\\subset ','⊂', 'subset'); - -LatexCmds.sup = LatexCmds.supset = LatexCmds.superset = - bind(BinaryOperator,'\\supset ','⊃', 'superset'); - -LatexCmds.nsub = LatexCmds.notsub = -LatexCmds.nsubset = LatexCmds.notsubset = - bind(BinaryOperator,'\\not\\subset ','⊄', 'not a subset'); - -LatexCmds.nsup = LatexCmds.notsup = -LatexCmds.nsupset = LatexCmds.notsupset = -LatexCmds.nsuperset = LatexCmds.notsuperset = - bind(BinaryOperator,'\\not\\supset ','⊅', 'not a superset'); - -LatexCmds.sube = LatexCmds.subeq = LatexCmds.subsete = LatexCmds.subseteq = - bind(BinaryOperator,'\\subseteq ','⊆', 'subset or equal to'); - -LatexCmds.supe = LatexCmds.supeq = -LatexCmds.supsete = LatexCmds.supseteq = -LatexCmds.supersete = LatexCmds.superseteq = - bind(BinaryOperator,'\\supseteq ','⊇', 'superset or equal to'); - -LatexCmds.nsube = LatexCmds.nsubeq = -LatexCmds.notsube = LatexCmds.notsubeq = -LatexCmds.nsubsete = LatexCmds.nsubseteq = -LatexCmds.notsubsete = LatexCmds.notsubseteq = - bind(BinaryOperator,'\\not\\subseteq ','⊈', 'not subset or equal to'); - -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 ','⊉', 'not superset or equal to'); - -//the canonical sets of numbers -LatexCmds.mathbb = P(MathCommand, function(_) { - _.createLeftOf = noop; - _.numBlocks = function() { return 1; }; - _.parser = function() { - var string = Parser.string; - var regex = Parser.regex; - var optWhitespace = Parser.optWhitespace; - return optWhitespace.then(string('{')) - .then(optWhitespace) - .then(regex(/^[NPZQRCH]/)) - .skip(optWhitespace) - .skip(string('}')) - .map(function(c) { - // instantiate the class for the matching char - return LatexCmds[c](); - }); - }; -}); - -LatexCmds.N = LatexCmds.naturals = LatexCmds.Naturals = - bind(VanillaSymbol,'\\mathbb{N}','ℕ', 'naturals'); - -LatexCmds.P = -LatexCmds.primes = LatexCmds.Primes = -LatexCmds.projective = LatexCmds.Projective = -LatexCmds.probability = LatexCmds.Probability = - bind(VanillaSymbol,'\\mathbb{P}','ℙ', 'P'); - -LatexCmds.Z = LatexCmds.integers = LatexCmds.Integers = - bind(VanillaSymbol,'\\mathbb{Z}','ℤ', 'integers'); - -LatexCmds.Q = LatexCmds.rationals = LatexCmds.Rationals = - bind(VanillaSymbol,'\\mathbb{Q}','ℚ', 'rationals'); - -LatexCmds.R = LatexCmds.reals = LatexCmds.Reals = - bind(VanillaSymbol,'\\mathbb{R}','ℝ', 'reals'); - -LatexCmds.C = -LatexCmds.complex = LatexCmds.Complex = -LatexCmds.complexes = LatexCmds.Complexes = -LatexCmds.complexplane = LatexCmds.Complexplane = LatexCmds.ComplexPlane = - bind(VanillaSymbol,'\\mathbb{C}','ℂ', 'complexes'); - -LatexCmds.H = LatexCmds.Hamiltonian = LatexCmds.quaternions = LatexCmds.Quaternions = - bind(VanillaSymbol,'\\mathbb{H}','ℍ', 'quaternions'); - -//spacing -LatexCmds.quad = LatexCmds.emsp = bind(VanillaSymbol,'\\quad ',' ', '4 spaces'); -LatexCmds.qquad = bind(VanillaSymbol,'\\qquad ',' ', '8 spaces'); -/* spacing special characters, gonna have to implement this in LatexCommandInput::onText somehow -case ',': - return VanillaSymbol('\\, ',' ', 'comma'); -case ':': - return VanillaSymbol('\\: ',' ', 'colon'); -case ';': - return VanillaSymbol('\\; ',' ', 'semicolon'); -case '!': - return Symbol('\\! ','', 'exclamation point'); -*/ - -//binary operators -LatexCmds.diamond = bind(VanillaSymbol, '\\diamond ', '◇', 'diamond'); -LatexCmds.bigtriangleup = bind(VanillaSymbol, '\\bigtriangleup ', '△', 'triangle up'); -LatexCmds.ominus = bind(VanillaSymbol, '\\ominus ', '⊖', 'o minus'); -LatexCmds.uplus = bind(VanillaSymbol, '\\uplus ', '⊎', 'disjoint union'); -LatexCmds.bigtriangledown = bind(VanillaSymbol, '\\bigtriangledown ', '▽', 'triangle down'); -LatexCmds.sqcap = bind(VanillaSymbol, '\\sqcap ', '⊓', 'greatest lower bound'); -LatexCmds.triangleleft = bind(VanillaSymbol, '\\triangleleft ', '⊲', 'triangle left'); -LatexCmds.sqcup = bind(VanillaSymbol, '\\sqcup ', '⊔', 'least upper bound'); -LatexCmds.triangleright = bind(VanillaSymbol, '\\triangleright ', '⊳', 'triangle right'); -//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 ', '⊙', 'circle dot'); -LatexCmds.bigcirc = bind(VanillaSymbol, '\\bigcirc ', '◯', 'circle'); -LatexCmds.dagger = bind(VanillaSymbol, '\\dagger ', '†', 'dagger'); -LatexCmds.ddagger = bind(VanillaSymbol, '\\ddagger ', '‡', 'big dagger'); -LatexCmds.wr = bind(VanillaSymbol, '\\wr ', '≀', 'wreath'); -LatexCmds.amalg = bind(VanillaSymbol, '\\amalg ', '∐', 'amalgam'); - -//relationship symbols -LatexCmds.models = bind(VanillaSymbol, '\\models ', '⊨', 'models'); -LatexCmds.prec = bind(VanillaSymbol, '\\prec ', '≺', 'precedes'); -LatexCmds.succ = bind(VanillaSymbol, '\\succ ', '≻', 'succeeds'); -LatexCmds.preceq = bind(VanillaSymbol, '\\preceq ', '≼', 'precedes or equals'); -LatexCmds.succeq = bind(VanillaSymbol, '\\succeq ', '≽', 'succeeds or equals'); -LatexCmds.simeq = bind(VanillaSymbol, '\\simeq ', '≃', 'similar or equal to'); -LatexCmds.mid = bind(VanillaSymbol, '\\mid ', '∣', 'divides'); -LatexCmds.ll = bind(VanillaSymbol, '\\ll ', '≪', 'll'); -LatexCmds.gg = bind(VanillaSymbol, '\\gg ', '≫', 'gg'); -LatexCmds.parallel = bind(VanillaSymbol, '\\parallel ', '∥', 'parallel with'); -LatexCmds.nparallel = bind(VanillaSymbol, '\\nparallel ', '∦', 'not parallel with'); -LatexCmds.bowtie = bind(VanillaSymbol, '\\bowtie ', '⋈', 'bowtie'); -LatexCmds.sqsubset = bind(VanillaSymbol, '\\sqsubset ', '⊏', 'square subset'); -LatexCmds.sqsupset = bind(VanillaSymbol, '\\sqsupset ', '⊐', 'square superset'); -LatexCmds.smile = bind(VanillaSymbol, '\\smile ', '⌣', 'smile'); -LatexCmds.sqsubseteq = bind(VanillaSymbol, '\\sqsubseteq ', '⊑', 'square subset or equal to'); -LatexCmds.sqsupseteq = bind(VanillaSymbol, '\\sqsupseteq ', '⊒', 'square superset or equal to'); -LatexCmds.doteq = bind(VanillaSymbol, '\\doteq ', '≐', 'dotted equals'); -LatexCmds.frown = bind(VanillaSymbol, '\\frown ', '⌢', 'frown'); -LatexCmds.vdash = bind(VanillaSymbol, '\\vdash ', '⊦', 'v dash'); -LatexCmds.dashv = bind(VanillaSymbol, '\\dashv ', '⊣', 'dash v'); -LatexCmds.nless = bind(VanillaSymbol, '\\nless ', '≮', 'not less than'); -LatexCmds.ngtr = bind(VanillaSymbol, '\\ngtr ', '≯', 'not greater than'); - -//arrows -LatexCmds.longleftarrow = bind(VanillaSymbol, '\\longleftarrow ', '←', 'left arrow'); -LatexCmds.longrightarrow = bind(VanillaSymbol, '\\longrightarrow ', '→', 'right arrow'); -LatexCmds.Longleftarrow = bind(VanillaSymbol, '\\Longleftarrow ', '⇐', 'left arrow'); -LatexCmds.Longrightarrow = bind(VanillaSymbol, '\\Longrightarrow ', '⇒', 'right arrow'); -LatexCmds.longleftrightarrow = bind(VanillaSymbol, '\\longleftrightarrow ', '↔', 'left and right arrow'); -LatexCmds.updownarrow = bind(VanillaSymbol, '\\updownarrow ', '↕', 'up and down arrow'); -LatexCmds.Longleftrightarrow = bind(VanillaSymbol, '\\Longleftrightarrow ', '⇔', 'left and right arrow'); -LatexCmds.Updownarrow = bind(VanillaSymbol, '\\Updownarrow ', '⇕', 'up and down arrow'); -LatexCmds.mapsto = bind(VanillaSymbol, '\\mapsto ', '↦', 'maps to'); -LatexCmds.nearrow = bind(VanillaSymbol, '\\nearrow ', '↗', 'northeast arrow'); -LatexCmds.hookleftarrow = bind(VanillaSymbol, '\\hookleftarrow ', '↩', 'hook left arrow'); -LatexCmds.hookrightarrow = bind(VanillaSymbol, '\\hookrightarrow ', '↪', 'hook right arrow'); -LatexCmds.searrow = bind(VanillaSymbol, '\\searrow ', '↘', 'southeast arrow'); -LatexCmds.leftharpoonup = bind(VanillaSymbol, '\\leftharpoonup ', '↼', 'left harpoon up'); -LatexCmds.rightharpoonup = bind(VanillaSymbol, '\\rightharpoonup ', '⇀', 'right harpoon up'); -LatexCmds.swarrow = bind(VanillaSymbol, '\\swarrow ', '↙', 'southwest arrow'); -LatexCmds.leftharpoondown = bind(VanillaSymbol, '\\leftharpoondown ', '↽', 'left harpoon down'); -LatexCmds.rightharpoondown = bind(VanillaSymbol, '\\rightharpoondown ', '⇁', 'right harpoon down'); -LatexCmds.nwarrow = bind(VanillaSymbol, '\\nwarrow ', '↖', 'northwest arrow'); - -//Misc -LatexCmds.ldots = bind(VanillaSymbol, '\\ldots ', '…', 'l dots'); -LatexCmds.cdots = bind(VanillaSymbol, '\\cdots ', '⋯', 'c dots'); -LatexCmds.vdots = bind(VanillaSymbol, '\\vdots ', '⋮', 'v dots'); -LatexCmds.ddots = bind(VanillaSymbol, '\\ddots ', '⋱', 'd dots'); -LatexCmds.surd = bind(VanillaSymbol, '\\surd ', '√', 'unresolved root'); -LatexCmds.triangle = bind(VanillaSymbol, '\\triangle ', '△', 'triangle'); -LatexCmds.ell = bind(VanillaSymbol, '\\ell ', 'ℓ', 'ell'); -LatexCmds.top = bind(VanillaSymbol, '\\top ', '⊤', 'top'); -LatexCmds.flat = bind(VanillaSymbol, '\\flat ', '♭', 'flat'); -LatexCmds.natural = bind(VanillaSymbol, '\\natural ', '♮', 'natural'); -LatexCmds.sharp = bind(VanillaSymbol, '\\sharp ', '♯', 'sharp'); -LatexCmds.wp = bind(VanillaSymbol, '\\wp ', '℘', 'wp'); -LatexCmds.bot = bind(VanillaSymbol, '\\bot ', '⊥', 'bot'); -LatexCmds.clubsuit = bind(VanillaSymbol, '\\clubsuit ', '♣', 'club suit'); -LatexCmds.diamondsuit = bind(VanillaSymbol, '\\diamondsuit ', '♢', 'diamond suit'); -LatexCmds.heartsuit = bind(VanillaSymbol, '\\heartsuit ', '♡', 'heart suit'); -LatexCmds.spadesuit = bind(VanillaSymbol, '\\spadesuit ', '♠', 'spade suit'); -//not real LaTex command see https://github.com/mathquill/mathquill/pull/552 for more details -LatexCmds.parallelogram = bind(VanillaSymbol, '\\parallelogram ', '▱', 'parallelogram'); -LatexCmds.square = bind(VanillaSymbol, '\\square ', '⬜', 'square'); - -//variable-sized -LatexCmds.oint = bind(VanillaSymbol, '\\oint ', '∮', 'o int'); -LatexCmds.bigcap = bind(VanillaSymbol, '\\bigcap ', '∩', 'big cap'); -LatexCmds.bigcup = bind(VanillaSymbol, '\\bigcup ', '∪', 'big cup'); -LatexCmds.bigsqcup = bind(VanillaSymbol, '\\bigsqcup ', '⊔', 'big square cup'); -LatexCmds.bigvee = bind(VanillaSymbol, '\\bigvee ', '∨', 'big vee'); -LatexCmds.bigwedge = bind(VanillaSymbol, '\\bigwedge ', '∧', 'big wedge'); -LatexCmds.bigodot = bind(VanillaSymbol, '\\bigodot ', '⊙', 'big o dot'); -LatexCmds.bigotimes = bind(VanillaSymbol, '\\bigotimes ', '⊗', 'big o times'); -LatexCmds.bigoplus = bind(VanillaSymbol, '\\bigoplus ', '⊕', 'big o plus'); -LatexCmds.biguplus = bind(VanillaSymbol, '\\biguplus ', '⊎', 'big u plus'); - -//delimiters -LatexCmds.lfloor = bind(VanillaSymbol, '\\lfloor ', '⌊', 'left floor'); -LatexCmds.rfloor = bind(VanillaSymbol, '\\rfloor ', '⌋', 'right floor'); -LatexCmds.lceil = bind(VanillaSymbol, '\\lceil ', '⌈', 'left ceiling'); -LatexCmds.rceil = bind(VanillaSymbol, '\\rceil ', '⌉', 'right ceiling'); -LatexCmds.opencurlybrace = LatexCmds.lbrace = bind(VanillaSymbol, '\\lbrace ', '{', 'left brace'); -LatexCmds.closecurlybrace = LatexCmds.rbrace = bind(VanillaSymbol, '\\rbrace ', '}', 'right brace'); -LatexCmds.lbrack = bind(VanillaSymbol, '[', 'left bracket'); -LatexCmds.rbrack = bind(VanillaSymbol, ']', 'right bracket'); - -//various symbols -LatexCmds.slash = bind(VanillaSymbol, '/', 'slash'); -LatexCmds.vert = bind(VanillaSymbol,'|', 'vertical bar'); -LatexCmds.perp = LatexCmds.perpendicular = bind(VanillaSymbol,'\\perp ','⊥', 'perpendicular'); -LatexCmds.nabla = LatexCmds.del = bind(VanillaSymbol,'\\nabla ','∇'); -LatexCmds.hbar = bind(VanillaSymbol,'\\hbar ','ℏ', 'horizontal bar'); - -LatexCmds.AA = LatexCmds.Angstrom = LatexCmds.angstrom = - bind(VanillaSymbol,'\\text\\AA ','Å', 'AA'); - -LatexCmds.ring = LatexCmds.circ = LatexCmds.circle = - bind(VanillaSymbol,'\\circ ','∘', 'circle'); - -LatexCmds.bull = LatexCmds.bullet = bind(VanillaSymbol,'\\bullet ','•', 'bullet'); - -LatexCmds.setminus = LatexCmds.smallsetminus = - bind(VanillaSymbol,'\\setminus ','∖', 'set minus'); - -LatexCmds.not = //bind(Symbol,'\\not ','/', 'not'); -LatexCmds['¬'] = LatexCmds.neg = bind(VanillaSymbol,'\\neg ','¬', 'not'); - -LatexCmds['…'] = LatexCmds.dots = LatexCmds.ellip = LatexCmds.hellip = -LatexCmds.ellipsis = LatexCmds.hellipsis = - bind(VanillaSymbol,'\\dots ','…', 'ellipsis'); - -LatexCmds.converges = -LatexCmds.darr = LatexCmds.dnarr = LatexCmds.dnarrow = LatexCmds.downarrow = - bind(VanillaSymbol,'\\downarrow ','↓', 'converges with'); - -LatexCmds.dArr = LatexCmds.dnArr = LatexCmds.dnArrow = LatexCmds.Downarrow = - bind(VanillaSymbol,'\\Downarrow ','⇓', 'down arrow'); - -LatexCmds.diverges = LatexCmds.uarr = LatexCmds.uparrow = - bind(VanillaSymbol,'\\uparrow ','↑', 'diverges from'); - -LatexCmds.uArr = LatexCmds.Uparrow = bind(VanillaSymbol,'\\Uparrow ','⇑', 'up arrow'); - -LatexCmds.rarr = LatexCmds.rightarrow = bind(VanillaSymbol,'\\rightarrow ','→', 'right arrow'); - -LatexCmds.implies = bind(BinaryOperator,'\\Rightarrow ','⇒', 'implies'); - -LatexCmds.rArr = LatexCmds.Rightarrow = bind(VanillaSymbol,'\\Rightarrow ','⇒', 'right arrow'); - -LatexCmds.gets = bind(BinaryOperator,'\\gets ','←', 'gets'); - -LatexCmds.larr = LatexCmds.leftarrow = bind(VanillaSymbol,'\\leftarrow ','←', 'left arrow'); - -LatexCmds.impliedby = bind(BinaryOperator,'\\Leftarrow ','⇐', 'implied by'); - -LatexCmds.lArr = LatexCmds.Leftarrow = bind(VanillaSymbol,'\\Leftarrow ','⇐', 'left arrow'); - -LatexCmds.harr = LatexCmds.lrarr = LatexCmds.leftrightarrow = - bind(VanillaSymbol,'\\leftrightarrow ','↔', 'left and right arrow'); - -LatexCmds.iff = bind(BinaryOperator,'\\Leftrightarrow ','⇔', 'if and only if'); - -LatexCmds.hArr = LatexCmds.lrArr = LatexCmds.Leftrightarrow = - bind(VanillaSymbol,'\\Leftrightarrow ','⇔', 'left and right arrow'); - -LatexCmds.Re = LatexCmds.Real = LatexCmds.real = bind(VanillaSymbol,'\\Re ','ℜ', 'real'); - -LatexCmds.Im = LatexCmds.imag = -LatexCmds.image = LatexCmds.imagin = LatexCmds.imaginary = LatexCmds.Imaginary = - bind(VanillaSymbol,'\\Im ','ℑ', 'imaginary'); - -LatexCmds.part = LatexCmds.partial = bind(VanillaSymbol,'\\partial ','∂', 'partial'); - -LatexCmds.pounds = bind(VanillaSymbol,'\\pounds ','£'); - -LatexCmds.alef = LatexCmds.alefsym = LatexCmds.aleph = LatexCmds.alephsym = - bind(VanillaSymbol,'\\aleph ','ℵ', 'alef sym'); - -LatexCmds.xist = //LOL -LatexCmds.xists = LatexCmds.exist = LatexCmds.exists = - bind(VanillaSymbol,'\\exists ','∃', 'there exists at least 1'); - -LatexCmds.nexists = LatexCmds.nexist = - bind(VanillaSymbol, '\\nexists ', '∄', 'there is no'); - -LatexCmds.and = LatexCmds.land = LatexCmds.wedge = - bind(BinaryOperator,'\\wedge ','∧', 'and'); - -LatexCmds.or = LatexCmds.lor = LatexCmds.vee = bind(BinaryOperator,'\\vee ','∨', 'or'); - -LatexCmds.o = LatexCmds.O = -LatexCmds.empty = LatexCmds.emptyset = -LatexCmds.oslash = LatexCmds.Oslash = -LatexCmds.nothing = LatexCmds.varnothing = - bind(BinaryOperator,'\\varnothing ','∅', 'nothing'); - -LatexCmds.cup = LatexCmds.union = bind(BinaryOperator,'\\cup ','∪', 'union'); - -LatexCmds.cap = LatexCmds.intersect = LatexCmds.intersection = - bind(BinaryOperator,'\\cap ','∩', 'intersection'); - -// FIXME: the correct LaTeX would be ^\circ but we can't parse that -LatexCmds.deg = LatexCmds.degree = bind(VanillaSymbol,'\\degree ','°', 'degrees'); - -LatexCmds.ang = LatexCmds.angle = bind(VanillaSymbol,'\\angle ','∠', 'angle'); -LatexCmds.measuredangle = bind(VanillaSymbol,'\\measuredangle ','∡', 'measured angle'); diff --git a/src/commands/math/advancedSymbols.ts b/src/commands/math/advancedSymbols.ts new file mode 100644 index 000000000..3c430a21e --- /dev/null +++ b/src/commands/math/advancedSymbols.ts @@ -0,0 +1,339 @@ +/************************************ + * Symbols for Advanced Mathematics + ***********************************/ + +LatexCmds.notin = +LatexCmds.cong = +LatexCmds.equiv = +LatexCmds.oplus = +LatexCmds.otimes = (latex:string) => new BinaryOperator('\\'+latex+' ', '&'+latex+';'); + +LatexCmds['∗'] = LatexCmds.ast = LatexCmds.star = LatexCmds.loast = LatexCmds.lowast = + bindBinaryOperator('\\ast ','∗', 'low asterisk'); +LatexCmds.therefor = LatexCmds.therefore = + bindBinaryOperator('\\therefore ','∴', 'therefore'); + +LatexCmds.cuz = // l33t +LatexCmds.because = bindBinaryOperator('\\because ','∵', 'because'); + +LatexCmds.prop = LatexCmds.propto = bindBinaryOperator('\\propto ','∝', 'proportional to'); + +LatexCmds['≈'] = LatexCmds.asymp = LatexCmds.approx = bindBinaryOperator('\\approx ','≈', 'approximately equal to'); + +LatexCmds.isin = LatexCmds['in'] = bindBinaryOperator('\\in ','∈', 'is in'); + +LatexCmds.ni = LatexCmds.contains = bindBinaryOperator('\\ni ','∋', 'is not in'); + +LatexCmds.notni = LatexCmds.niton = LatexCmds.notcontains = LatexCmds.doesnotcontain = + bindBinaryOperator('\\not\\ni ','∌', 'does not contain'); + +LatexCmds.sub = LatexCmds.subset = bindBinaryOperator('\\subset ','⊂', 'subset'); + +LatexCmds.sup = LatexCmds.supset = LatexCmds.superset = + bindBinaryOperator('\\supset ','⊃', 'superset'); + +LatexCmds.nsub = LatexCmds.notsub = +LatexCmds.nsubset = LatexCmds.notsubset = + bindBinaryOperator('\\not\\subset ','⊄', 'not a subset'); + +LatexCmds.nsup = LatexCmds.notsup = +LatexCmds.nsupset = LatexCmds.notsupset = +LatexCmds.nsuperset = LatexCmds.notsuperset = + bindBinaryOperator('\\not\\supset ','⊅', 'not a superset'); + +LatexCmds.sube = LatexCmds.subeq = LatexCmds.subsete = LatexCmds.subseteq = + bindBinaryOperator('\\subseteq ','⊆', 'subset or equal to'); + +LatexCmds.supe = LatexCmds.supeq = +LatexCmds.supsete = LatexCmds.supseteq = +LatexCmds.supersete = LatexCmds.superseteq = + bindBinaryOperator('\\supseteq ','⊇', 'superset or equal to'); + +LatexCmds.nsube = LatexCmds.nsubeq = +LatexCmds.notsube = LatexCmds.notsubeq = +LatexCmds.nsubsete = LatexCmds.nsubseteq = +LatexCmds.notsubsete = LatexCmds.notsubseteq = + bindBinaryOperator('\\not\\subseteq ','⊈', 'not subset or equal to'); + +LatexCmds.nsupe = LatexCmds.nsupeq = +LatexCmds.notsupe = LatexCmds.notsupeq = +LatexCmds.nsupsete = LatexCmds.nsupseteq = +LatexCmds.notsupsete = LatexCmds.notsupseteq = +LatexCmds.nsupersete = LatexCmds.nsuperseteq = +LatexCmds.notsupersete = LatexCmds.notsuperseteq = + bindBinaryOperator('\\not\\supseteq ','⊉', 'not superset or equal to'); + +//the canonical sets of numbers +LatexCmds.mathbb = class extends MathCommand { + createLeftOf (_cursor:Cursor) {}; + numBlocks () { return 1; }; + parser () { + var string = Parser.string; + var regex = Parser.regex; + var optWhitespace = Parser.optWhitespace; + return optWhitespace.then(string('{')) + .then(optWhitespace) + .then(regex(/^[NPZQRCH]/)) + .skip(optWhitespace) + .skip(string('}')) + .map(function(c) { + // instantiate the class for the matching char + var cmd = LatexCmds[c]; + if (isMQNodeClass(cmd)) { + return new cmd(); + } else { + return (cmd as MQNodeBuilderNoParam)(); + } + }); + }; +}; + +LatexCmds.N = LatexCmds.naturals = LatexCmds.Naturals = + bindVanillaSymbol('\\mathbb{N}','ℕ', 'naturals'); + +LatexCmds.P = +LatexCmds.primes = LatexCmds.Primes = +LatexCmds.projective = LatexCmds.Projective = +LatexCmds.probability = LatexCmds.Probability = + bindVanillaSymbol('\\mathbb{P}','ℙ', 'P'); + +LatexCmds.Z = LatexCmds.integers = LatexCmds.Integers = + bindVanillaSymbol('\\mathbb{Z}','ℤ', 'integers'); + +LatexCmds.Q = LatexCmds.rationals = LatexCmds.Rationals = + bindVanillaSymbol('\\mathbb{Q}','ℚ', 'rationals'); + +LatexCmds.R = LatexCmds.reals = LatexCmds.Reals = + bindVanillaSymbol('\\mathbb{R}','ℝ', 'reals'); + +LatexCmds.C = +LatexCmds.complex = LatexCmds.Complex = +LatexCmds.complexes = LatexCmds.Complexes = +LatexCmds.complexplane = LatexCmds.Complexplane = LatexCmds.ComplexPlane = + bindVanillaSymbol('\\mathbb{C}','ℂ', 'complexes'); + +LatexCmds.H = LatexCmds.Hamiltonian = LatexCmds.quaternions = LatexCmds.Quaternions = + bindVanillaSymbol('\\mathbb{H}','ℍ', 'quaternions'); + +//spacing +LatexCmds.quad = LatexCmds.emsp = bindVanillaSymbol('\\quad ',' ', '4 spaces'); +LatexCmds.qquad = bindVanillaSymbol('\\qquad ',' ', '8 spaces'); +/* spacing special characters, gonna have to implement this in LatexCommandInput::onText somehow +case ',': + return VanillaSymbol('\\, ',' ', 'comma'); +case ':': + return VanillaSymbol('\\: ',' ', 'colon'); +case ';': + return VanillaSymbol('\\; ',' ', 'semicolon'); +case '!': + return MQSymbol('\\! ','', 'exclamation point'); +*/ + +//binary operators +LatexCmds.diamond = bindVanillaSymbol('\\diamond ', '◇', 'diamond'); +LatexCmds.bigtriangleup = bindVanillaSymbol('\\bigtriangleup ', '△', 'triangle up'); +LatexCmds.ominus = bindVanillaSymbol('\\ominus ', '⊖', 'o minus'); +LatexCmds.uplus = bindVanillaSymbol('\\uplus ', '⊎', 'disjoint union'); +LatexCmds.bigtriangledown = bindVanillaSymbol('\\bigtriangledown ', '▽', 'triangle down'); +LatexCmds.sqcap = bindVanillaSymbol('\\sqcap ', '⊓', 'greatest lower bound'); +LatexCmds.triangleleft = bindVanillaSymbol('\\triangleleft ', '⊲', 'triangle left'); +LatexCmds.sqcup = bindVanillaSymbol('\\sqcup ', '⊔', 'least upper bound'); +LatexCmds.triangleright = bindVanillaSymbol('\\triangleright ', '⊳', 'triangle right'); +//circledot is not a not real LaTex command see https://github.com/mathquill/mathquill/pull/552 for more details +LatexCmds.odot = LatexCmds.circledot = bindVanillaSymbol('\\odot ', '⊙', 'circle dot'); +LatexCmds.bigcirc = bindVanillaSymbol('\\bigcirc ', '◯', 'circle'); +LatexCmds.dagger = bindVanillaSymbol('\\dagger ', '†', 'dagger'); +LatexCmds.ddagger = bindVanillaSymbol('\\ddagger ', '‡', 'big dagger'); +LatexCmds.wr = bindVanillaSymbol('\\wr ', '≀', 'wreath'); +LatexCmds.amalg = bindVanillaSymbol('\\amalg ', '∐', 'amalgam'); + +//relationship symbols +LatexCmds.models = bindVanillaSymbol('\\models ', '⊨', 'models'); +LatexCmds.prec = bindVanillaSymbol('\\prec ', '≺', 'precedes'); +LatexCmds.succ = bindVanillaSymbol('\\succ ', '≻', 'succeeds'); +LatexCmds.preceq = bindVanillaSymbol('\\preceq ', '≼', 'precedes or equals'); +LatexCmds.succeq = bindVanillaSymbol('\\succeq ', '≽', 'succeeds or equals'); +LatexCmds.simeq = bindVanillaSymbol('\\simeq ', '≃', 'similar or equal to'); +LatexCmds.mid = bindVanillaSymbol('\\mid ', '∣', 'divides'); +LatexCmds.ll = bindVanillaSymbol('\\ll ', '≪', 'll'); +LatexCmds.gg = bindVanillaSymbol('\\gg ', '≫', 'gg'); +LatexCmds.parallel = bindVanillaSymbol('\\parallel ', '∥', 'parallel with'); +LatexCmds.nparallel = bindVanillaSymbol('\\nparallel ', '∦', 'not parallel with'); +LatexCmds.bowtie = bindVanillaSymbol('\\bowtie ', '⋈', 'bowtie'); +LatexCmds.sqsubset = bindVanillaSymbol('\\sqsubset ', '⊏', 'square subset'); +LatexCmds.sqsupset = bindVanillaSymbol('\\sqsupset ', '⊐', 'square superset'); +LatexCmds.smile = bindVanillaSymbol('\\smile ', '⌣', 'smile'); +LatexCmds.sqsubseteq = bindVanillaSymbol('\\sqsubseteq ', '⊑', 'square subset or equal to'); +LatexCmds.sqsupseteq = bindVanillaSymbol('\\sqsupseteq ', '⊒', 'square superset or equal to'); +LatexCmds.doteq = bindVanillaSymbol('\\doteq ', '≐', 'dotted equals'); +LatexCmds.frown = bindVanillaSymbol('\\frown ', '⌢', 'frown'); +LatexCmds.vdash = bindVanillaSymbol('\\vdash ', '⊦', 'v dash'); +LatexCmds.dashv = bindVanillaSymbol('\\dashv ', '⊣', 'dash v'); +LatexCmds.nless = bindVanillaSymbol('\\nless ', '≮', 'not less than'); +LatexCmds.ngtr = bindVanillaSymbol('\\ngtr ', '≯', 'not greater than'); + +//arrows +LatexCmds.longleftarrow = bindVanillaSymbol('\\longleftarrow ', '←', 'left arrow'); +LatexCmds.longrightarrow = bindVanillaSymbol('\\longrightarrow ', '→', 'right arrow'); +LatexCmds.Longleftarrow = bindVanillaSymbol('\\Longleftarrow ', '⇐', 'left arrow'); +LatexCmds.Longrightarrow = bindVanillaSymbol('\\Longrightarrow ', '⇒', 'right arrow'); +LatexCmds.longleftrightarrow = bindVanillaSymbol('\\longleftrightarrow ', '↔', 'left and right arrow'); +LatexCmds.updownarrow = bindVanillaSymbol('\\updownarrow ', '↕', 'up and down arrow'); +LatexCmds.Longleftrightarrow = bindVanillaSymbol('\\Longleftrightarrow ', '⇔', 'left and right arrow'); +LatexCmds.Updownarrow = bindVanillaSymbol('\\Updownarrow ', '⇕', 'up and down arrow'); +LatexCmds.mapsto = bindVanillaSymbol('\\mapsto ', '↦', 'maps to'); +LatexCmds.nearrow = bindVanillaSymbol('\\nearrow ', '↗', 'northeast arrow'); +LatexCmds.hookleftarrow = bindVanillaSymbol('\\hookleftarrow ', '↩', 'hook left arrow'); +LatexCmds.hookrightarrow = bindVanillaSymbol('\\hookrightarrow ', '↪', 'hook right arrow'); +LatexCmds.searrow = bindVanillaSymbol('\\searrow ', '↘', 'southeast arrow'); +LatexCmds.leftharpoonup = bindVanillaSymbol('\\leftharpoonup ', '↼', 'left harpoon up'); +LatexCmds.rightharpoonup = bindVanillaSymbol('\\rightharpoonup ', '⇀', 'right harpoon up'); +LatexCmds.swarrow = bindVanillaSymbol('\\swarrow ', '↙', 'southwest arrow'); +LatexCmds.leftharpoondown = bindVanillaSymbol('\\leftharpoondown ', '↽', 'left harpoon down'); +LatexCmds.rightharpoondown = bindVanillaSymbol('\\rightharpoondown ', '⇁', 'right harpoon down'); +LatexCmds.nwarrow = bindVanillaSymbol('\\nwarrow ', '↖', 'northwest arrow'); + +//Misc +LatexCmds.ldots = bindVanillaSymbol('\\ldots ', '…', 'l dots'); +LatexCmds.cdots = bindVanillaSymbol('\\cdots ', '⋯', 'c dots'); +LatexCmds.vdots = bindVanillaSymbol('\\vdots ', '⋮', 'v dots'); +LatexCmds.ddots = bindVanillaSymbol('\\ddots ', '⋱', 'd dots'); +LatexCmds.surd = bindVanillaSymbol('\\surd ', '√', 'unresolved root'); +LatexCmds.triangle = bindVanillaSymbol('\\triangle ', '△', 'triangle'); +LatexCmds.ell = bindVanillaSymbol('\\ell ', 'ℓ', 'ell'); +LatexCmds.top = bindVanillaSymbol('\\top ', '⊤', 'top'); +LatexCmds.flat = bindVanillaSymbol('\\flat ', '♭', 'flat'); +LatexCmds.natural = bindVanillaSymbol('\\natural ', '♮', 'natural'); +LatexCmds.sharp = bindVanillaSymbol('\\sharp ', '♯', 'sharp'); +LatexCmds.wp = bindVanillaSymbol('\\wp ', '℘', 'wp'); +LatexCmds.bot = bindVanillaSymbol('\\bot ', '⊥', 'bot'); +LatexCmds.clubsuit = bindVanillaSymbol('\\clubsuit ', '♣', 'club suit'); +LatexCmds.diamondsuit = bindVanillaSymbol('\\diamondsuit ', '♢', 'diamond suit'); +LatexCmds.heartsuit = bindVanillaSymbol('\\heartsuit ', '♡', 'heart suit'); +LatexCmds.spadesuit = bindVanillaSymbol('\\spadesuit ', '♠', 'spade suit'); +//not real LaTex command see https://github.com/mathquill/mathquill/pull/552 for more details +LatexCmds.parallelogram = bindVanillaSymbol('\\parallelogram ', '▱', 'parallelogram'); +LatexCmds.square = bindVanillaSymbol('\\square ', '⬜', 'square'); + +//variable-sized +LatexCmds.oint = bindVanillaSymbol('\\oint ', '∮', 'o int'); +LatexCmds.bigcap = bindVanillaSymbol('\\bigcap ', '∩', 'big cap'); +LatexCmds.bigcup = bindVanillaSymbol('\\bigcup ', '∪', 'big cup'); +LatexCmds.bigsqcup = bindVanillaSymbol('\\bigsqcup ', '⊔', 'big square cup'); +LatexCmds.bigvee = bindVanillaSymbol('\\bigvee ', '∨', 'big vee'); +LatexCmds.bigwedge = bindVanillaSymbol('\\bigwedge ', '∧', 'big wedge'); +LatexCmds.bigodot = bindVanillaSymbol('\\bigodot ', '⊙', 'big o dot'); +LatexCmds.bigotimes = bindVanillaSymbol('\\bigotimes ', '⊗', 'big o times'); +LatexCmds.bigoplus = bindVanillaSymbol('\\bigoplus ', '⊕', 'big o plus'); +LatexCmds.biguplus = bindVanillaSymbol('\\biguplus ', '⊎', 'big u plus'); + +//delimiters +LatexCmds.lfloor = bindVanillaSymbol('\\lfloor ', '⌊', 'left floor'); +LatexCmds.rfloor = bindVanillaSymbol('\\rfloor ', '⌋', 'right floor'); +LatexCmds.lceil = bindVanillaSymbol('\\lceil ', '⌈', 'left ceiling'); +LatexCmds.rceil = bindVanillaSymbol('\\rceil ', '⌉', 'right ceiling'); +LatexCmds.opencurlybrace = LatexCmds.lbrace = bindVanillaSymbol('\\lbrace ', '{', 'left brace'); +LatexCmds.closecurlybrace = LatexCmds.rbrace = bindVanillaSymbol('\\rbrace ', '}', 'right brace'); +LatexCmds.lbrack = bindVanillaSymbol('[', 'left bracket'); +LatexCmds.rbrack = bindVanillaSymbol(']', 'right bracket'); + +//various symbols +LatexCmds.slash = bindVanillaSymbol('/', 'slash'); +LatexCmds.vert = bindVanillaSymbol('|', 'vertical bar'); +LatexCmds.perp = LatexCmds.perpendicular = bindVanillaSymbol('\\perp ','⊥', 'perpendicular'); +LatexCmds.nabla = LatexCmds.del = bindVanillaSymbol('\\nabla ','∇'); +LatexCmds.hbar = bindVanillaSymbol('\\hbar ','ℏ', 'horizontal bar'); + +LatexCmds.AA = LatexCmds.Angstrom = LatexCmds.angstrom = + bindVanillaSymbol('\\text\\AA ','Å', 'AA'); + +LatexCmds.ring = LatexCmds.circ = LatexCmds.circle = + bindVanillaSymbol('\\circ ','∘', 'circle'); + +LatexCmds.bull = LatexCmds.bullet = bindVanillaSymbol('\\bullet ','•', 'bullet'); + +LatexCmds.setminus = LatexCmds.smallsetminus = + bindVanillaSymbol('\\setminus ','∖', 'set minus'); + +LatexCmds.not = //bind(MQSymbol,'\\not ','/', 'not'); +LatexCmds['¬'] = LatexCmds.neg = bindVanillaSymbol('\\neg ','¬', 'not'); + +LatexCmds['…'] = LatexCmds.dots = LatexCmds.ellip = LatexCmds.hellip = +LatexCmds.ellipsis = LatexCmds.hellipsis = + bindVanillaSymbol('\\dots ','…', 'ellipsis'); + +LatexCmds.converges = +LatexCmds.darr = LatexCmds.dnarr = LatexCmds.dnarrow = LatexCmds.downarrow = + bindVanillaSymbol('\\downarrow ','↓', 'converges with'); + +LatexCmds.dArr = LatexCmds.dnArr = LatexCmds.dnArrow = LatexCmds.Downarrow = + bindVanillaSymbol('\\Downarrow ','⇓', 'down arrow'); + +LatexCmds.diverges = LatexCmds.uarr = LatexCmds.uparrow = + bindVanillaSymbol('\\uparrow ','↑', 'diverges from'); + +LatexCmds.uArr = LatexCmds.Uparrow = bindVanillaSymbol('\\Uparrow ','⇑', 'up arrow'); + +LatexCmds.rarr = LatexCmds.rightarrow = bindVanillaSymbol('\\rightarrow ','→', 'right arrow'); + +LatexCmds.implies = bindBinaryOperator('\\Rightarrow ','⇒', 'implies'); + +LatexCmds.rArr = LatexCmds.Rightarrow = bindVanillaSymbol('\\Rightarrow ','⇒', 'right arrow'); + +LatexCmds.gets = bindBinaryOperator('\\gets ','←', 'gets'); + +LatexCmds.larr = LatexCmds.leftarrow = bindVanillaSymbol('\\leftarrow ','←', 'left arrow'); + +LatexCmds.impliedby = bindBinaryOperator('\\Leftarrow ','⇐', 'implied by'); + +LatexCmds.lArr = LatexCmds.Leftarrow = bindVanillaSymbol('\\Leftarrow ','⇐', 'left arrow'); + +LatexCmds.harr = LatexCmds.lrarr = LatexCmds.leftrightarrow = + bindVanillaSymbol('\\leftrightarrow ','↔', 'left and right arrow'); + +LatexCmds.iff = bindBinaryOperator('\\Leftrightarrow ','⇔', 'if and only if'); + +LatexCmds.hArr = LatexCmds.lrArr = LatexCmds.Leftrightarrow = + bindVanillaSymbol('\\Leftrightarrow ','⇔', 'left and right arrow'); + +LatexCmds.Re = LatexCmds.Real = LatexCmds.real = bindVanillaSymbol('\\Re ','ℜ', 'real'); + +LatexCmds.Im = LatexCmds.imag = +LatexCmds.image = LatexCmds.imagin = LatexCmds.imaginary = LatexCmds.Imaginary = + bindVanillaSymbol('\\Im ','ℑ', 'imaginary'); + +LatexCmds.part = LatexCmds.partial = bindVanillaSymbol('\\partial ','∂', 'partial'); + +LatexCmds.pounds = bindVanillaSymbol('\\pounds ','£'); + +LatexCmds.alef = LatexCmds.alefsym = LatexCmds.aleph = LatexCmds.alephsym = + bindVanillaSymbol('\\aleph ','ℵ', 'alef sym'); + +LatexCmds.xist = //LOL +LatexCmds.xists = LatexCmds.exist = LatexCmds.exists = + bindVanillaSymbol('\\exists ','∃', 'there exists at least 1'); + +LatexCmds.nexists = LatexCmds.nexist = + bindVanillaSymbol('\\nexists ', '∄', 'there is no'); + +LatexCmds.and = LatexCmds.land = LatexCmds.wedge = + bindBinaryOperator('\\wedge ','∧', 'and'); + +LatexCmds.or = LatexCmds.lor = LatexCmds.vee = bindBinaryOperator('\\vee ','∨', 'or'); + +LatexCmds.o = LatexCmds.O = +LatexCmds.empty = LatexCmds.emptyset = +LatexCmds.oslash = LatexCmds.Oslash = +LatexCmds.nothing = LatexCmds.varnothing = + bindBinaryOperator('\\varnothing ','∅', 'nothing'); + +LatexCmds.cup = LatexCmds.union = bindBinaryOperator('\\cup ','∪', 'union'); + +LatexCmds.cap = LatexCmds.intersect = LatexCmds.intersection = + bindBinaryOperator('\\cap ','∩', 'intersection'); + +// FIXME: the correct LaTeX would be ^\circ but we can't parse that +LatexCmds.deg = LatexCmds.degree = bindVanillaSymbol('\\degree ','°', 'degrees'); + +LatexCmds.ang = LatexCmds.angle = bindVanillaSymbol('\\angle ','∠', 'angle'); +LatexCmds.measuredangle = bindVanillaSymbol('\\measuredangle ','∡', 'measured angle'); diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.ts similarity index 55% rename from src/commands/math/basicSymbols.js rename to src/commands/math/basicSymbols.ts index 45b16c62d..80b90cb53 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.ts @@ -1,19 +1,23 @@ /********************************* * Symbols for Basic Mathematics ********************************/ -var DigitGroupingChar = P(Symbol, function(_, super_) { - _.finalizeTree = _.siblingDeleted = _.siblingCreated = function(opts, dir) { +class DigitGroupingChar extends MQSymbol { + finalizeTree (opts:CursorOptions, dir:Direction) { this.sharedSiblingMethod(opts, dir) }; + siblingDeleted (opts:CursorOptions, dir:Direction) { this.sharedSiblingMethod(opts, dir) }; + siblingCreated (opts:CursorOptions, dir:Direction) { this.sharedSiblingMethod(opts, dir) }; + + sharedSiblingMethod (opts:CursorOptions, dir:Direction) { // don't try to fix digit grouping if the sibling to my right changed (dir === R or // undefined) and it's now a DigitGroupingChar, it will try to fix grouping if (dir !== L && this[R] instanceof DigitGroupingChar) return; this.fixDigitGrouping(opts); }; - _.fixDigitGrouping = function (opts) { + fixDigitGrouping (opts:CursorOptions) { if (!opts.enableDigitGrouping) return; - var left = this; - var right = this; + var left:NodeRef = this; + var right:NodeRef = this; var spacesFound = 0; var dots = []; @@ -22,9 +26,9 @@ var DigitGroupingChar = P(Symbol, function(_, super_) { var DOT = '.'; // traverse left as far as possible (starting at this char) - var node = left; + var node:NodeRef = left; do { - if (/^[0-9]$/.test(node.ctrlSeq)) { + if (/^[0-9]$/.test(node.ctrlSeq!)) { left = node } else if (node.ctrlSeq === SPACE) { left = node @@ -39,7 +43,7 @@ var DigitGroupingChar = P(Symbol, function(_, super_) { // traverse right as far as possible (starting to right of this char) while (node = right[R]) { - if (/^[0-9]$/.test(node.ctrlSeq)) { + if (/^[0-9]$/.test(node.ctrlSeq!)) { right = node } else if (node.ctrlSeq === SPACE) { right = node @@ -53,19 +57,19 @@ var DigitGroupingChar = P(Symbol, function(_, super_) { } // trim the leading spaces - while (right !== left && left.ctrlSeq === SPACE) { + while (right !== left && left && left.ctrlSeq === SPACE) { left = left[R]; spacesFound -= 1; } // trim the trailing spaces - while (right !== left && right.ctrlSeq === SPACE) { + while (right !== left && right && right.ctrlSeq === SPACE) { right = right[L]; spacesFound -= 1; } // happens when you only have a space - if (left === right && left.ctrlSeq === SPACE) return; + if (left === right && left && left.ctrlSeq === SPACE) return; var disableFormatting = spacesFound > 0 || dots.length > 1; if (disableFormatting) { @@ -83,15 +87,17 @@ var DigitGroupingChar = P(Symbol, function(_, super_) { } }; - _.removeGroupingBetween = function (left, right) { + removeGroupingBetween (left:NodeRef, right:NodeRef) { var node = left; do { - node.setGroupingClass(undefined); - if (node === right) break; + if (node instanceof DigitGroupingChar) { + node.setGroupingClass(undefined); + } + if (!node || node === right) break; } while (node = node[R]); }; - _.addGroupingBetween = function (start, end) { + addGroupingBetween (start:NodeRef, end:NodeRef) { var node = start; var count = 0; @@ -128,14 +134,17 @@ var DigitGroupingChar = P(Symbol, function(_, super_) { } } - node.setGroupingClass(cls); + if (node instanceof DigitGroupingChar) { + node.setGroupingClass(cls); + } if (node === end) break; - node = node[L]; + node = node[L] as DigitGroupingChar; } }; - _.setGroupingClass = function (cls) { + _groupingClass?:string; + setGroupingClass (cls:string | undefined) { // nothing changed (either class is the same or it's still undefined) if (this._groupingClass === cls) return; @@ -148,48 +157,66 @@ var DigitGroupingChar = P(Symbol, function(_, super_) { // cache the groupingClass this._groupingClass = cls; } -}); +}; -var Digit = P(DigitGroupingChar, function(_, super_) { - _.init = function(ch, html, mathspeak) { - super_.init.call(this, ch, ''+(html || ch)+'', undefined, mathspeak); +class Digit extends DigitGroupingChar { + constructor (ch:string, html?:string, mathspeak?:string) { + super(ch, ''+(html || ch)+'', undefined, mathspeak); }; - _.createLeftOf = function(cursor) { + createLeftOf (cursor:Cursor) { + const cursorL = cursor[L]; + const cursorLL = cursorL && cursorL[L]; + const cursorParentParentSub = ( + cursor.parent.parent instanceof SupSub ? + cursor.parent.parent.sub + : undefined + ) + 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.parent !== cursorParentParentSub + && ((cursorL instanceof Variable && cursorL.isItalic !== false) + || (cursorL instanceof SupSub + && cursorLL instanceof Variable + && cursorLL.isItalic !== false))) { + new SubscriptCommand().createLeftOf(cursor); + super.createLeftOf(cursor); cursor.insRightOf(cursor.parent.parent); } - else super_.createLeftOf.call(this, cursor); + else super.createLeftOf(cursor); }; - _.mathspeak = function(opts) { + mathspeak (opts:MathspeakOptions) { if (opts && opts.createdLeftOf) { var cursor = opts.createdLeftOf; + var cursorL = cursor[L]; + var cursorLL = cursorL && cursorL[L]; + const cursorParentParentSub = ( + cursor.parent.parent instanceof SupSub ? + cursor.parent.parent.sub + : undefined + ) + if (cursor.options.autoSubscriptNumerals - && cursor.parent !== cursor.parent.parent.sub - && ((cursor[L] instanceof Variable && cursor[L].isItalic !== false) + && cursor.parent !== cursorParentParentSub + && ((cursorL instanceof Variable && cursorL.isItalic !== false) || (cursor[L] instanceof SupSub - && cursor[L][L] instanceof Variable - && cursor[L][L].isItalic !== false))) { - return 'Subscript ' + super_.mathspeak.call(this) + ' Baseline'; + && cursorLL instanceof Variable + && cursorLL.isItalic !== false))) { + return 'Subscript ' + super.mathspeak() + ' Baseline'; } } - return super_.mathspeak.apply(this, arguments); + return super.mathspeak(); }; -}); +} + +class Variable extends MQSymbol { + isItalic?:boolean; -var Variable = P(Symbol, function(_, super_) { - _.init = function(ch, html) { - super_.init.call(this, ch, ''+(html || ch)+''); + constructor (ch:string, html?:string) { + super(ch, ''+(html || ch)+''); }; - _.text = function() { - var text = this.ctrlSeq; + text () { + var text = this.ctrlSeq || ''; if (this.isPartOfOperator) { if (text[0] == '\\') { text = text.slice(1, text.length); @@ -200,7 +227,7 @@ var Variable = P(Symbol, function(_, super_) { } else { if (this[L] && !(this[L] instanceof Variable) && !(this[L] instanceof BinaryOperator) - && this[L].ctrlSeq !== '\\ ') + && (this[L] as MQNode).ctrlSeq !== '\\ ') text = '*' + text; if (this[R] && !(this[R] instanceof BinaryOperator) && !(this[R] instanceof SupSub)) @@ -208,14 +235,14 @@ var Variable = P(Symbol, function(_, super_) { } return text; }; - _.mathspeak = function() { - var text = this.ctrlSeq; + mathspeak () { + var text = this.ctrlSeq || ''; if ( this.isPartOfOperator || text.length > 1 || (this.parent && this.parent.parent && this.parent.parent.isTextBlock()) ) { - return super_.mathspeak.call(this); + return super.mathspeak(); } else { // Apple voices in VoiceOver (such as Alex, Bruce, and Victoria) do // some strange pronunciation given certain expressions, @@ -225,19 +252,27 @@ var Variable = P(Symbol, function(_, super_) { return '"'+text+'"'; } }; -}); +}; +function bindVariable (ch:string, html:string, _unusedMathspeak?:string) { + return () => new Variable(ch, html); +} + -Options.p.autoCommands = { _maxLength: 0 }; -optionProcessors.autoCommands = function(cmds) { +Options.prototype.autoCommands = { _maxLength: 0 }; +optionProcessors.autoCommands = function(cmds:string) { 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; + var list = cmds.split(' '); + var dict:AutoDict = {} + var 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'; } @@ -248,12 +283,25 @@ optionProcessors.autoCommands = function(cmds) { return dict; }; -Options.p.autoParenthesizedFunctions = {_maxLength: 0}; +Options.prototype.quietEmptyDelimiters = {}; +optionProcessors.quietEmptyDelimiters = function(dlms:string) { + var list = dlms.split(' '); + var dict: { [id:string]:any; } = {}; + for (var i = 0; i < list.length; i += 1) { + var dlm = list[i]; + dict[dlm] = 1; + } + return dict; +}; + +Options.prototype.autoParenthesizedFunctions = {_maxLength: 0}; optionProcessors.autoParenthesizedFunctions = 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; + var list = cmds.split(' '); + var dict:AutoDict = {} + var maxLength = 0; for (var i = 0; i < list.length; i += 1) { var cmd = list[i]; if (cmd.length < 2) { @@ -266,48 +314,77 @@ optionProcessors.autoParenthesizedFunctions = function (cmds) { return dict; } -var Letter = P(Variable, function(_, super_) { - _.init = function(ch) { return super_.init.call(this, this.letter = ch); }; - _.checkAutoCmds = function (cursor) { +class Letter extends Variable { + letter:string; + + constructor (ch:string) { + super(ch); + this.letter = ch; + }; + checkAutoCmds (cursor:Cursor) { + //exit early if in simple subscript and disableAutoSubstitutionInSubscripts is set. + if (this.shouldIgnoreSubstitutionInSimpleSubscript(cursor.options)) { + return; + } + //handle autoCommands - var autoCmds = cursor.options.autoCommands, maxLength = autoCmds._maxLength; + var autoCmds = cursor.options.autoCommands; + var maxLength = autoCmds._maxLength || 0; if (maxLength > 0) { // want longest possible autocommand, so join together longest // sequence of letters - var str = '', l = this, i = 0; + var str = ''; + var l:NodeRef = this; + var 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; + 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); + l = this; + for (i = 1; l && i < str.length; i += 1, l = l[L]); + + new Fragment(l as MQNode, this).remove(); + cursor[L] = (l as MQNode)[L]; + + var cmd = LatexCmds[str]; + var node; + if (isMQNodeClass(cmd)) { + node = new (cmd as typeof TempSingleCharNode)(str); // TODO - How do we know that this class expects a single str input? + } else { + node = cmd(str); + } + + return node.createLeftOf(cursor); } str = str.slice(1); } } } - _.autoParenthesize = function (cursor) { + autoParenthesize (cursor:Cursor) { //exit early if already parenthesized var right = cursor.parent.ends[R] if (right && right instanceof Bracket && right.ctrlSeq === '\\left(') { return } - //exit early if in simple subscript - if (this.isParentSimpleSubscript()) { + //exit early if in simple subscript and disableAutoSubstitutionInSubscripts is set. + if (this.shouldIgnoreSubstitutionInSimpleSubscript(cursor.options)) { return; } //handle autoParenthesized functions - var str = '', l = this, i = 0; + var str = ''; + var l:NodeRef = this + var i = 0; - var autoParenthesizedFunctions = cursor.options.autoParenthesizedFunctions, maxLength = autoParenthesizedFunctions._maxLength; + var autoParenthesizedFunctions = cursor.options.autoParenthesizedFunctions; + var maxLength = autoParenthesizedFunctions._maxLength || 0; var autoOperatorNames = cursor.options.autoOperatorNames while (l instanceof Letter && i < maxLength) { str = l.letter + str, l = l[L], i += 1; @@ -322,30 +399,35 @@ var Letter = P(Variable, function(_, super_) { } } - _.createLeftOf = function(cursor) { - super_.createLeftOf.apply(this, arguments); + createLeftOf (cursor:Cursor) { + super.createLeftOf(cursor); this.checkAutoCmds(cursor); this.autoParenthesize(cursor); }; - _.italicize = function(bool) { + italicize (bool:boolean) { this.isItalic = bool; this.isPartOfOperator = !bool; this.jQ.toggleClass('mq-operator-name', !bool); return this; }; - _.finalizeTree = _.siblingDeleted = _.siblingCreated = function(opts, dir) { + finalizeTree (opts:CursorOptions, dir:Direction) {this.sharedSiblingMethod(opts, dir)}; + siblingDeleted (opts:CursorOptions, dir:Direction) {this.sharedSiblingMethod(opts, dir)}; + siblingCreated (opts:CursorOptions, dir:Direction) {this.sharedSiblingMethod(opts, dir)}; + + sharedSiblingMethod (opts:CursorOptions, dir:Direction) { // 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) { + + autoUnItalicize (opts:CursorOptions) { var autoOps = opts.autoOperatorNames; if (autoOps._maxLength === 0) return; - //exit early if in simple subscript - if (this.isParentSimpleSubscript()) { + //exit early if in simple subscript and disableAutoSubstitutionInSubscripts is set. + if (this.shouldIgnoreSubstitutionInSimpleSubscript(opts)) { return; } @@ -357,30 +439,50 @@ var Letter = P(Variable, function(_, super_) { // 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; + var lR = l && l[R]; + var rL = r && r[L]; + + new Fragment(lR || this.parent.ends[L] as MQNode, rL || this.parent.ends[R] as MQNode).each(function(el) { + if (el instanceof Letter) { + el.italicize(true).jQ.removeClass('mq-first mq-last mq-followed-by-supsub'); + el.ctrlSeq = el.letter; + } + return undefined; }); + let autoOpsLength = autoOps._maxLength || 0; + // 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) { + outer: for (var i = 0, first = (l as MQNode)[R] || this.parent.ends[L]; first && i < str.length; i += 1, first = (first as MQNode)[R]) { + for (var len = min(autoOpsLength, str.length - i); len > 0; len -= 1) { var word = str.slice(i, i + len); + var last:MQNode = undefined!; // TODO - TS complaining that we use last before assigning to it + if (autoOps.hasOwnProperty(word)) { - for (var j = 0, letter = first; j < len; j += 1, letter = letter[R]) { - letter.italicize(false); - var last = letter; + for (var j = 0, letter:NodeRef = first; j < len; j += 1, letter = (letter as MQNode)[R]) { + if (letter instanceof Letter) { + letter.italicize(false); + 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 (TwoWordOpNames.hasOwnProperty(word)) { + const lastL = last[L]; + const lastLL = lastL && lastL[L]; + const lastLLL = (lastLL && lastLL[L]) as MQNode; + lastLLL.jQ.addClass('mq-last'); + } + + if (!this.shouldOmitPadding(first[L])) first.jQ.addClass('mq-first'); + if (!this.shouldOmitPadding(last[R])) { if (last[R] instanceof SupSub) { - var supsub = last[R]; // XXX monkey-patching, but what's the right thing here? + var supsub = last[R] as MQNode; // 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() { @@ -400,7 +502,7 @@ var Letter = P(Variable, function(_, super_) { } } }; - function shouldOmitPadding(node) { + shouldOmitPadding(node:NodeRef) { // omit padding if no node if (!node) return true; @@ -415,14 +517,14 @@ var Letter = P(Variable, function(_, super_) { return false; } -}); -var BuiltInOpNames = {}; // the set of operator names like \sin, \cos, etc that +}; +var BuiltInOpNames:AutoDict = {}; // 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 +var AutoOpNames:AutoDict = Options.prototype.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() { @@ -462,7 +564,9 @@ optionProcessors.autoOperatorNames = function(cmds) { if (!/^[a-z\|\-]+(?: [a-z\|\-]+)*$/i.test(cmds)) { throw '"'+cmds+'" not a space-delimited list of letters or "|"'; } - var list = cmds.split(' '), dict = {}, maxLength = 0; + var list = cmds.split(' '); + var dict:AutoDict = {}; + var maxLength = 0; for (var i = 0; i < list.length; i += 1) { var cmd = list[i]; if (cmd.length < 2) { @@ -487,30 +591,33 @@ optionProcessors.autoOperatorNames = function(cmds) { dict._maxLength = maxLength; return dict; }; -var OperatorName = P(Symbol, function(_, super_) { - _.init = function(fn) { this.ctrlSeq = fn; }; - _.createLeftOf = function(cursor) { +class OperatorName extends MQSymbol { + ctrlSeq:string; + constructor (fn?:string) { + super(fn || ''); + }; + createLeftOf (cursor:Cursor) { var fn = this.ctrlSeq; for (var i = 0; i < fn.length; i += 1) { - Letter(fn.charAt(i)).createLeftOf(cursor); + new Letter(fn.charAt(i)).createLeftOf(cursor); } }; - _.parser = function() { + parser () { var fn = this.ctrlSeq; - var block = MathBlock(); + var block = new MathBlock(); for (var i = 0; i < fn.length; i += 1) { - Letter(fn.charAt(i)).adopt(block, block.ends[R], 0); + new Letter(fn.charAt(i)).adopt(block, block.ends[R], 0); } - return Parser.succeed(block.children()); + return Parser.succeed(block.children()) as ParserAny; }; -}); +}; for (var fn in AutoOpNames) if (AutoOpNames.hasOwnProperty(fn)) { - LatexCmds[fn] = OperatorName; + (LatexCmds as LatexCmdsAny)[fn as string] = OperatorName; } -LatexCmds.operatorname = P(MathCommand, function(_) { - _.createLeftOf = noop; - _.numBlocks = function() { return 1; }; - _.parser = function() { +LatexCmds.operatorname = class extends MathCommand { + createLeftOf () {}; + numBlocks () { return 1; }; + parser () { return latexMathParser.block.map(function(b) { // Check for the special case of \operatorname{ans}, which has // a special html representation @@ -523,62 +630,62 @@ LatexCmds.operatorname = P(MathCommand, function(_) { } else { isAllLetters = false; } + return undefined; }); - if (isAllLetters && str === 'ans') return LatexCmds[str](str); + if (isAllLetters && str === 'ans') { + return AnsBuilder(); + } // In cases other than `ans`, just return the children directly return children; - }); + }) as ParserAny; }; -}); +}; + +LatexCmds.f = class extends Letter { + letter:string; + constructor() { + var letter = 'f'; + super(letter); -LatexCmds.f = P(Letter, function(_, super_) { - _.init = function() { - Symbol.p.init.call(this, this.letter = 'f', 'f'); + this.letter = letter; + this.htmlTemplate = 'f'; }; - _.italicize = function(bool) { + italicize (bool:boolean) { this.jQ.html('f').toggleClass('mq-f', bool); - return super_.italicize.apply(this, arguments); + return super.italicize(bool); }; -}); +}; // VanillaSymbol's -LatexCmds[' '] = LatexCmds.space = P(DigitGroupingChar, function(_, super_) { - _.init = function () { - super_.init.call(this, '\\ ', ' ', ' '); - }; -}); +LatexCmds[' '] = LatexCmds.space = () => new DigitGroupingChar('\\ ', ' ', ' '); -LatexCmds['.'] = P(DigitGroupingChar, function(_, super_) { - _.init = function () { - super_.init.call(this, '.', '.', '.'); - }; -}); +LatexCmds['.'] = () => new DigitGroupingChar('.', '.', '.') -LatexCmds["'"] = LatexCmds.prime = bind(VanillaSymbol, "'", '′', 'prime'); -LatexCmds['″'] = LatexCmds.dprime = bind(VanillaSymbol, '″', '″', 'double prime'); +LatexCmds["'"] = LatexCmds.prime = bindVanillaSymbol("'", '′', 'prime'); +LatexCmds['″'] = LatexCmds.dprime = bindVanillaSymbol('″', '″', 'double prime'); -LatexCmds.backslash = bind(VanillaSymbol,'\\backslash ','\\', 'backslash'); +LatexCmds.backslash = bindVanillaSymbol('\\backslash ','\\', 'backslash'); if (!CharCmds['\\']) CharCmds['\\'] = LatexCmds.backslash; -LatexCmds.$ = bind(VanillaSymbol, '\\$', '$', 'dollar'); +LatexCmds.$ = bindVanillaSymbol('\\$', '$', 'dollar'); -LatexCmds.square = bind(VanillaSymbol, '\\square ', '\u25A1', 'square'); -LatexCmds.mid = bind(VanillaSymbol, '\\mid ', '\u2223', 'mid'); +LatexCmds.square = bindVanillaSymbol('\\square ', '\u25A1', 'square'); +LatexCmds.mid = bindVanillaSymbol('\\mid ', '\u2223', 'mid'); // does not use Symbola font -var NonSymbolaSymbol = P(Symbol, function(_, super_) { - _.init = function(ch, html) { - super_.init.call(this, ch, ''+(html || ch)+''); +class NonSymbolaSymbol extends MQSymbol { + constructor (ch:string, html?:string, _unusedMathspeak?:string) { + super(ch, ''+(html || ch)+''); }; -}); +}; -LatexCmds['@'] = NonSymbolaSymbol; -LatexCmds['&'] = bind(NonSymbolaSymbol, '\\&', '&', 'and'); -LatexCmds['%'] = P(NonSymbolaSymbol, function(_, super_) { - _.init = function () { - super_.init.call(this, '\\%', '%', 'percent'); +LatexCmds['@'] = () => new NonSymbolaSymbol('@'); +LatexCmds['&'] = () => new NonSymbolaSymbol('\\&', '&', 'and'); +LatexCmds['%'] = class extends NonSymbolaSymbol { + constructor() { + super('\\%', '%', 'percent'); }; - _.parser = function () { + parser () { var optWhitespace = Parser.optWhitespace; var string = Parser.string; @@ -588,21 +695,21 @@ LatexCmds['%'] = P(NonSymbolaSymbol, function(_, super_) { .then( string('\\operatorname{of}') .map(function () { - return LatexCmds.percentof(); + return PercentOfBuilder(); }) - ).or(super_.parser.call(this)) + ).or(super.parser()) as ParserAny ; } -}); +}; LatexCmds['∥'] = LatexCmds.parallel = - bind(VanillaSymbol, '\\parallel ', '∥', 'parallel'); + bindVanillaSymbol('\\parallel ', '∥', 'parallel'); LatexCmds['∦'] = LatexCmds.nparallel = - bind(VanillaSymbol, '\\nparallel ', '∦', 'not parallel'); + bindVanillaSymbol('\\nparallel ', '∦', 'not parallel'); LatexCmds['⟂'] = LatexCmds.perp = - bind(VanillaSymbol, '\\perp ', '⟂', 'perpendicular'); + bindVanillaSymbol('\\perp ', '⟂', 'perpendicular'); //the following are all Greek to me, but this helped a lot: http://www.ams.org/STIX/ion/stixsig03.html @@ -624,62 +731,58 @@ LatexCmds.sigma = LatexCmds.tau = LatexCmds.chi = LatexCmds.psi = -LatexCmds.omega = P(Variable, function(_, super_) { - _.init = function(latex) { - super_.init.call(this,'\\'+latex+' ','&'+latex+';'); - }; -}); +LatexCmds.omega = (latex) => new Variable('\\'+latex+' ','&'+latex+';'); //why can't anybody FUCKING agree on these LatexCmds.phi = //W3C or Unicode? - bind(Variable,'\\phi ','ϕ', 'phi'); + bindVariable('\\phi ','ϕ', 'phi'); LatexCmds.phiv = //Elsevier and 9573-13 LatexCmds.varphi = //AMS and LaTeX - bind(Variable,'\\varphi ','φ', 'phi'); + bindVariable('\\varphi ','φ', 'phi'); LatexCmds.epsilon = //W3C or Unicode? - bind(Variable,'\\epsilon ','ϵ', 'epsilon'); + bindVariable('\\epsilon ','ϵ', 'epsilon'); LatexCmds.epsiv = //Elsevier and 9573-13 LatexCmds.varepsilon = //AMS and LaTeX - bind(Variable,'\\varepsilon ','ε', 'epsilon'); + bindVariable('\\varepsilon ','ε', 'epsilon'); LatexCmds.piv = //W3C/Unicode and Elsevier and 9573-13 LatexCmds.varpi = //AMS and LaTeX - bind(Variable,'\\varpi ','ϖ', 'piv'); + bindVariable('\\varpi ','ϖ', 'piv'); LatexCmds.sigmaf = //W3C/Unicode LatexCmds.sigmav = //Elsevier LatexCmds.varsigma = //LaTeX - bind(Variable,'\\varsigma ','ς', 'sigma'); + bindVariable('\\varsigma ','ς', 'sigma'); LatexCmds.thetav = //Elsevier and 9573-13 LatexCmds.vartheta = //AMS and LaTeX LatexCmds.thetasym = //W3C/Unicode - bind(Variable,'\\vartheta ','ϑ', 'theta'); + bindVariable('\\vartheta ','ϑ', 'theta'); LatexCmds.upsilon = //AMS and LaTeX and W3C/Unicode LatexCmds.upsi = //Elsevier and 9573-13 - bind(Variable,'\\upsilon ','υ', 'upsilon'); + bindVariable('\\upsilon ','υ', '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 ','ϝ', 'gamma'); + bindVariable('\\digamma ','ϝ', 'gamma'); LatexCmds.kappav = //Elsevier LatexCmds.varkappa = //AMS and LaTeX - bind(Variable,'\\varkappa ','ϰ', 'kappa'); + bindVariable('\\varkappa ','ϰ', 'kappa'); LatexCmds.rhov = //Elsevier and 9573-13 LatexCmds.varrho = //AMS and LaTeX - bind(Variable,'\\varrho ','ϱ', 'rho'); + bindVariable('\\varrho ','ϱ', 'rho'); //Greek constants, look best in non-italicized Times New Roman -LatexCmds.pi = LatexCmds['π'] = bind(NonSymbolaSymbol,'\\pi ','π', 'pi'); -LatexCmds.lambda = bind(NonSymbolaSymbol,'\\lambda ','λ', 'lambda'); +LatexCmds.pi = LatexCmds['π'] = () => new NonSymbolaSymbol('\\pi ','π', 'pi'); +LatexCmds.lambda = () => new NonSymbolaSymbol('\\lambda ','λ', 'lambda'); //uppercase greek letters @@ -687,7 +790,7 @@ 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 ','ϒ', 'capital upsilon'); //Symbola's 'upsilon with a hook' is a capital Y without hooks :( + () => new MQSymbol('\\Upsilon ','ϒ', 'capital upsilon'); //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 = @@ -700,32 +803,38 @@ LatexCmds.Sigma = LatexCmds.Phi = LatexCmds.Psi = LatexCmds.Omega = -LatexCmds.forall = P(VanillaSymbol, function(_, super_) { - _.init = function(latex) { - super_.init.call(this,'\\'+latex+' ','&'+latex+';'); - }; -}); +LatexCmds.forall = (latex) => new VanillaSymbol('\\'+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]); +class LatexFragment extends MathCommand { + latexStr:string; + + constructor (latex:string) { + super(); + this.latexStr = latex; + } + + createLeftOf (cursor:Cursor) { + var block = latexMathParser.parse(this.latexStr); + block.children().adopt(cursor.parent, cursor[L] as MQNode, cursor[R] as MQNode); 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(function (node) { node.reflow(); }); + var blockEndsR = block.ends[R]; + var blockEndsRR = blockEndsR && blockEndsR[R]; + if (blockEndsRR) blockEndsRR.siblingCreated(cursor.options, L); + var blockEndsL = block.ends[L]; + var blockEndsLL = blockEndsL && blockEndsL[L] ; + if (blockEndsLL) blockEndsLL.siblingCreated(cursor.options, R); + cursor.parent.bubble(function (node) { node.reflow(); return undefined; }); }; - _.mathspeak = function() { return latexMathParser.parse(this.latex).mathspeak(); }; - _.parser = function() { - var frag = latexMathParser.parse(this.latex).children(); - return Parser.succeed(frag); + mathspeak () { return latexMathParser.parse(this.latexStr).mathspeak(); }; + parser () { + var frag = latexMathParser.parse(this.latexStr).children(); + return Parser.succeed(frag) as ParserAny; }; -}); +}; // for what seems to me like [stupid reasons][1], Unicode provides // subscripted and superscripted versions of all ten Arabic numerals, @@ -751,20 +860,20 @@ var LatexFragment = P(MathCommand, function(_) { // [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['⁰'] = bind(LatexFragment, '^0'); -LatexCmds['¹'] = bind(LatexFragment, '^1'); -LatexCmds['²'] = bind(LatexFragment, '^2'); -LatexCmds['³'] = bind(LatexFragment, '^3'); -LatexCmds['⁴'] = bind(LatexFragment, '^4'); -LatexCmds['⁵'] = bind(LatexFragment, '^5'); -LatexCmds['⁶'] = bind(LatexFragment, '^6'); -LatexCmds['⁷'] = bind(LatexFragment, '^7'); -LatexCmds['⁸'] = bind(LatexFragment, '^8'); -LatexCmds['⁹'] = bind(LatexFragment, '^9'); - -LatexCmds['¼'] = bind(LatexFragment, '\\frac14'); -LatexCmds['½'] = bind(LatexFragment, '\\frac12'); -LatexCmds['¾'] = bind(LatexFragment, '\\frac34'); +LatexCmds['⁰'] = () => new LatexFragment('^0'); +LatexCmds['¹'] = () => new LatexFragment('^1'); +LatexCmds['²'] = () => new LatexFragment('^2'); +LatexCmds['³'] = () => new LatexFragment('^3'); +LatexCmds['⁴'] = () => new LatexFragment('^4'); +LatexCmds['⁵'] = () => new LatexFragment('^5'); +LatexCmds['⁶'] = () => new LatexFragment('^6'); +LatexCmds['⁷'] = () => new LatexFragment('^7'); +LatexCmds['⁸'] = () => new LatexFragment('^8'); +LatexCmds['⁹'] = () => new LatexFragment('^9'); + +LatexCmds['¼'] = () => new LatexFragment('\\frac14'); +LatexCmds['½'] = () => new LatexFragment('\\frac12'); +LatexCmds['¾'] = () => new LatexFragment('\\frac34'); // this is a hack to make pasting the √ symbol // actually insert a sqrt command. This isn't ideal, @@ -788,16 +897,20 @@ LatexCmds['¾'] = bind(LatexFragment, '\\frac34'); // act more like simply typing the characters out. I'd be scared to try // to make that change because I'm fairly confident I'd break something // around handling valid latex as latex rather than treating it as keystrokes. -LatexCmds['√'] = bind(LatexFragment, '\\sqrt{}'); +LatexCmds['√'] = () => new LatexFragment('\\sqrt{}'); // Binary operator determination is used in several contexts for PlusMinus nodes and their descendants. // For instance, we set the item's class name based on this factor, and also assign different mathspeak values (plus vs positive, negative vs minus). -function isBinaryOperator(node) { - if (node[L]) { +function isBinaryOperator(node:NodeRef):boolean { + if (!node) return false; + + const nodeL = node[L]; + + if (nodeL) { // If the left sibling is a binary operator or a separator (comma, semicolon, colon, space) // or an open bracket (open parenthesis, open square bracket) // consider the operator to be unary - if (node[L] instanceof BinaryOperator || /^(\\ )|[,;:\(\[]$/.test(node[L].ctrlSeq)) { + if (nodeL instanceof BinaryOperator || /^(\\ )|[,;:\(\[]$/.test(nodeL.ctrlSeq!)) { return false; } } else if (node.parent && node.parent.parent && node.parent.parent.isStyleBlock()) { @@ -812,10 +925,16 @@ function isBinaryOperator(node) { return true; } -var PlusMinus = P(BinaryOperator, function(_) { +var PlusMinus = class extends BinaryOperator { + constructor (ch?:string, html?:string, mathspeak?:string) { + super(ch, html, undefined, mathspeak, true); + }; - _.init = VanillaSymbol.prototype.init; - _.contactWeld = _.siblingCreated = _.siblingDeleted = function(opts, dir) { + contactWeld (cursor:Cursor, dir?:Direction) { this.sharedSiblingMethod(cursor.options, dir)} + siblingCreated (opts:CursorOptions, dir:Direction) { this.sharedSiblingMethod(opts, dir)} + siblingDeleted (opts:CursorOptions, dir:Direction) { this.sharedSiblingMethod(opts, dir)} + + sharedSiblingMethod (_opts?:CursorOptions, dir?:Direction) { if (dir === R) return; // ignore if sibling only changed on the right this.jQ[0].className = isBinaryOperator(this) ? 'mq-binary-operator' @@ -823,164 +942,172 @@ var PlusMinus = P(BinaryOperator, function(_) { return this; }; -}); +}; -LatexCmds['+'] = P(PlusMinus, function(_, super_) { - _.init = function () { - super_.init.call(this, '+', '+'); +LatexCmds['+'] = class extends PlusMinus { + constructor () { + super('+', '+'); }; - _.mathspeak = function() { + mathspeak ():string { return isBinaryOperator(this) ? 'plus' : 'positive'; }; -}); +}; //yes, these are different dashes, en-dash, em-dash, unicode minus, actual dash -LatexCmds['−'] = LatexCmds['—'] = LatexCmds['–'] = LatexCmds['-'] = P(PlusMinus, function(_, super_) { - _.init = function () { - super_.init.call(this, '-', '−'); +class MinusNode extends PlusMinus { + constructor () { + super('-', '−'); }; - _.mathspeak = function() { + mathspeak ():string { return isBinaryOperator(this) ? 'minus' : 'negative'; }; -}); +}; +LatexCmds['−'] = LatexCmds['—'] = LatexCmds['–'] = LatexCmds['-'] = MinusNode; LatexCmds['±'] = LatexCmds.pm = LatexCmds.plusmn = LatexCmds.plusminus = - bind(PlusMinus,'\\pm ','±', 'plus-or-minus'); + () => new PlusMinus('\\pm ','±', 'plus-or-minus'); LatexCmds.mp = LatexCmds.mnplus = LatexCmds.minusplus = - bind(PlusMinus,'\\mp ','∓', 'minus-or-plus'); + () => new PlusMinus('\\mp ','∓', 'minus-or-plus'); CharCmds['*'] = LatexCmds.sdot = LatexCmds.cdot = - bind(BinaryOperator, '\\cdot ', '·', '*', 'times'); //semantically should be ⋅, but · looks better + bindBinaryOperator('\\cdot ', '·', '*', 'times'); //semantically should be ⋅, but · looks better -var To = P(BinaryOperator, function(_, super_) { - _.init = function() { - super_.init.call(this, '\\to ','→', 'to'); +class To extends BinaryOperator { + constructor () { + super('\\to ','→', 'to'); } - _.deleteTowards = function(dir, cursor) { + deleteTowards (dir:Direction, cursor:Cursor) { if (dir === L) { - var l = cursor[L]; - Fragment(l, this).remove(); + var l = cursor[L] as MQNode; + new Fragment(l, this).remove(); cursor[L] = l[L]; - LatexCmds['−']().createLeftOf(cursor); - cursor[L].bubble(function (node) { node.reflow(); }); + new MinusNode().createLeftOf(cursor); + (cursor[L] as MQNode).bubble(function (node) { node.reflow(); return undefined }); return; } - super_.deleteTowards.apply(this, arguments); + super.deleteTowards(dir, cursor); }; -}) +} LatexCmds['→'] = LatexCmds.to = To; -var Inequality = P(BinaryOperator, function(_, super_) { - _.init = function(data, strict) { +class Inequality extends BinaryOperator { + strict:boolean; + data:InequalityData; + + constructor (data:InequalityData, strict:boolean) { + var strictness:''|'Strict' = (strict ? 'Strict' : ''); + super(data[`ctrlSeq${strictness}`], data[`html${strictness}`], + data[`text${strictness}`], data[`mathspeak${strictness}`]); + this.data = data; this.strict = strict; - var strictness = (strict ? 'Strict' : ''); - super_.init.call(this, data['ctrlSeq'+strictness], data['html'+strictness], - data['text'+strictness], data['mathspeak'+strictness]); - }; - _.swap = function(strict) { + } + + swap (strict:boolean) { 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] ]; - this.mathspeakName = this.data['mathspeak'+strictness]; + var strictness:''|'Strict' = (strict ? 'Strict' : ''); + this.ctrlSeq = this.data[`ctrlSeq${strictness}`]; + this.jQ.html(this.data[`html${strictness}`]); + this.textTemplate = [this.data[`text${strictness}`] ]; + this.mathspeakName = this.data[`mathspeak${strictness}`]; }; - _.deleteTowards = function(dir, cursor) { + deleteTowards (dir:Direction, cursor:Cursor) { if (dir === L && !this.strict) { this.swap(true); - this.bubble(function (node) { node.reflow(); }); + this.bubble(function (node) { node.reflow(); return undefined }); return; } - super_.deleteTowards.apply(this, arguments); + super.deleteTowards(dir, cursor); }; -}); +}; -var less = { ctrlSeq: '\\le ', html: '≤', text: '≤', mathspeak: 'less than or equal to', +var less:InequalityData = { ctrlSeq: '\\le ', html: '≤', text: '≤', mathspeak: 'less than or equal to', ctrlSeqStrict: '<', htmlStrict: '<', textStrict: '<', mathspeakStrict: 'less than'}; -var greater = { ctrlSeq: '\\ge ', html: '≥', text: '≥', mathspeak: 'greater than or equal to', +var greater:InequalityData = { ctrlSeq: '\\ge ', html: '≥', text: '≥', mathspeak: 'greater than or equal to', ctrlSeqStrict: '>', htmlStrict: '>', textStrict: '>', mathspeakStrict: 'greater than'}; -var Greater = P(Inequality, function(_, super_) { - _.init = function() { - super_.init.call(this, greater, true); +class Greater extends Inequality { + constructor () { + super(greater, true); }; - _.createLeftOf = function(cursor) { - if (cursor[L] instanceof BinaryOperator && cursor[L].ctrlSeq === '-') { - var l = cursor[L]; + createLeftOf (cursor:Cursor) { + const cursorL = cursor[L]; + if (cursorL instanceof BinaryOperator && cursorL.ctrlSeq === '-') { + var l = cursorL; cursor[L] = l[L]; l.remove(); - To().createLeftOf(cursor); - cursor[L].bubble(function (node) { node.reflow(); }); + new To().createLeftOf(cursor); + (cursor[L] as MQNode).bubble(function (node) { node.reflow(); return undefined }); return; } - super_.createLeftOf.apply(this, arguments); + super.createLeftOf(cursor); }; -}) +} -LatexCmds['<'] = LatexCmds.lt = bind(Inequality, less, true); +LatexCmds['<'] = LatexCmds.lt = () => new Inequality(less, true); LatexCmds['>'] = LatexCmds.gt = Greater; -LatexCmds['≤'] = LatexCmds.le = LatexCmds.leq = bind(Inequality, less, false); -LatexCmds['≥'] = LatexCmds.ge = LatexCmds.geq = bind(Inequality, greater, false); +LatexCmds['≤'] = LatexCmds.le = LatexCmds.leq = () => new Inequality(less, false); +LatexCmds['≥'] = LatexCmds.ge = LatexCmds.geq = () => new Inequality(greater, false); LatexCmds.infty = LatexCmds.infin = LatexCmds.infinity = - bind(VanillaSymbol,'\\infty ','∞', 'infinity'); -LatexCmds['≠'] = LatexCmds.ne = LatexCmds.neq = bind(BinaryOperator,'\\ne ','≠', 'not equal'); + bindVanillaSymbol('\\infty ','∞', 'infinity'); +LatexCmds['≠'] = LatexCmds.ne = LatexCmds.neq = bindBinaryOperator('\\ne ','≠', 'not equal'); -var Equality = P(BinaryOperator, function(_, super_) { - _.init = function() { - super_.init.call(this, '=', '=', '=', 'equals'); +class Equality extends BinaryOperator { + constructor () { + super('=', '=', '=', 'equals'); }; - _.createLeftOf = function(cursor) { - if (cursor[L] instanceof Inequality && cursor[L].strict) { - cursor[L].swap(false); - cursor[L].bubble(function (node) { node.reflow(); }); + createLeftOf (cursor:Cursor) { + var cursorL = cursor[L]; + if (cursorL instanceof Inequality && cursorL.strict) { + cursorL.swap(false); + cursorL.bubble(function (node) { node.reflow(); return undefined}); return; } - super_.createLeftOf.apply(this, arguments); + super.createLeftOf(cursor); }; -}); +}; LatexCmds['='] = Equality; -LatexCmds['×'] = LatexCmds.times = bind(BinaryOperator, '\\times ', '×', '[x]', 'times'); +LatexCmds['×'] = LatexCmds.times = bindBinaryOperator('\\times ', '×', '[x]', 'times'); LatexCmds['÷'] = LatexCmds.div = LatexCmds.divide = LatexCmds.divides = - bind(BinaryOperator,'\\div ','÷', '[/]', 'over'); + bindBinaryOperator('\\div ','÷', '[/]', 'over'); -var Sim = P(BinaryOperator, function(_, super_) { - _.init = function() { - super_.init.call(this, '\\sim ', '~', '~', 'tilde'); +class Sim extends BinaryOperator { + constructor () { + super('\\sim ', '~', '~', 'tilde'); }; - _.createLeftOf = function(cursor) { + createLeftOf (cursor:Cursor) { if (cursor[L] instanceof Sim) { - var l = cursor[L]; + var l = cursor[L] as MQNode; cursor[L] = l[L]; l.remove(); - Approx().createLeftOf(cursor); - cursor[L].bubble(function (node) { node.reflow(); }); + new Approx().createLeftOf(cursor); + (cursor[L] as MQNode).bubble(function (node) { node.reflow(); return undefined }); return; } - super_.createLeftOf.apply(this, arguments); + super.createLeftOf(cursor); }; -}); +}; -var Approx = P(BinaryOperator, function(_, super_) { - _.init = function() { - super_.init.call(this, '\\approx ', '≈', '≈', 'approximately equal'); +class Approx extends BinaryOperator { + constructor () { + super('\\approx ', '≈', '≈', 'approximately equal'); }; - _.deleteTowards = function(dir, cursor) { + deleteTowards (dir:Direction, cursor:Cursor) { if (dir === L) { - var l = cursor[L]; - Fragment(l, this).remove(); + var l = cursor[L] as MQNode; + new Fragment(l, this).remove(); cursor[L] = l[L]; - Sim().createLeftOf(cursor); - cursor[L].bubble(function (node) { node.reflow(); }); + new Sim().createLeftOf(cursor); + (cursor[L] as MQNode).bubble(function (node) { node.reflow(); return undefined }); return; } - super_.deleteTowards.apply(this, arguments); + super.deleteTowards(dir, cursor); }; -}); +}; CharCmds['~'] = LatexCmds.sim = Sim; LatexCmds['≈'] = LatexCmds.approx = Approx; diff --git a/src/commands/math/commands.js b/src/commands/math/commands.ts similarity index 54% rename from src/commands/math/commands.js rename to src/commands/math/commands.ts index 3a5214ac8..654045a23 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.ts @@ -3,6 +3,7 @@ **************************/ var SVG_SYMBOLS = { 'sqrt': { + width: '', html: '' + '' + @@ -80,57 +81,58 @@ var SVG_SYMBOLS = { } }; -var Style = P(MathCommand, function(_, super_) { - _.init = function(ctrlSeq, tagName, attrs, ariaLabel, opts) { - super_.init.call(this, ctrlSeq, '<'+tagName+' '+attrs+'>&0'); - _.ariaLabel = ariaLabel || ctrlSeq.replace(/^\\/, ''); - _.mathspeakTemplate = ['Start' + _.ariaLabel + ',', 'End' + _.ariaLabel]; +class Style extends MathCommand { + shouldNotSpeakDelimiters:boolean | undefined; + + constructor (ctrlSeq:string, tagName:string, attrs:string, ariaLabel?:string, opts?:{shouldNotSpeakDelimiters: boolean}) { + super(ctrlSeq, '<'+tagName+' '+attrs+'>&0'); + this.ariaLabel = ariaLabel || ctrlSeq.replace(/^\\/, ''); + this.mathspeakTemplate = ['Start' + this.ariaLabel + ',', 'End' + this.ariaLabel]; // In most cases, mathspeak should announce the start and end of style blocks. // There is one exception currently (mathrm). - _.shouldNotSpeakDelimiters = opts && opts.shouldNotSpeakDelimiters; + this.shouldNotSpeakDelimiters = opts && opts.shouldNotSpeakDelimiters; }; - _.mathspeak = function(opts) { + mathspeak (opts?:MathspeakOptions) { if ( !this.shouldNotSpeakDelimiters || (opts && opts.ignoreShorthand) ) { - return super_.mathspeak.call(this); + return super.mathspeak(); } return this.foldChildren('', function(speech, block) { return speech + ' ' + block.mathspeak(opts); }).trim(); }; -}); +}; //fonts -LatexCmds.mathrm = P(Style, function(_, super_) { - _.init = function() { - super_.init.call(this, '\\mathrm', 'span', 'class="mq-roman mq-font"', 'Roman Font', { shouldNotSpeakDelimiters: true }); +LatexCmds.mathrm = class extends Style { + constructor () { + super('\\mathrm', 'span', 'class="mq-roman mq-font"', 'Roman Font', { shouldNotSpeakDelimiters: true }); }; - _.isTextBlock = function() { + isTextBlock () { return true; }; -}); -LatexCmds.mathit = bind(Style, '\\mathit', 'i', 'class="mq-font"', 'Italic Font'); -LatexCmds.mathbf = bind(Style, '\\mathbf', 'b', 'class="mq-font"', 'Bold Font'); -LatexCmds.mathsf = bind(Style, '\\mathsf', 'span', 'class="mq-sans-serif mq-font"', 'Serif Font'); -LatexCmds.mathtt = bind(Style, '\\mathtt', 'span', 'class="mq-monospace mq-font"', 'Math Text'); +}; +LatexCmds.mathit = () => new Style('\\mathit', 'i', 'class="mq-font"', 'Italic Font'); +LatexCmds.mathbf = () => new Style('\\mathbf', 'b', 'class="mq-font"', 'Bold Font'); +LatexCmds.mathsf = () => new Style('\\mathsf', 'span', 'class="mq-sans-serif mq-font"', 'Serif Font'); +LatexCmds.mathtt = () => new Style('\\mathtt', 'span', 'class="mq-monospace mq-font"', 'Math Text'); //text-decoration -LatexCmds.underline = bind(Style, '\\underline', 'span', 'class="mq-non-leaf mq-underline"', 'Underline'); -LatexCmds.overline = LatexCmds.bar = bind(Style, '\\overline', 'span', 'class="mq-non-leaf mq-overline"', 'Overline'); -LatexCmds.overrightarrow = bind(Style, '\\overrightarrow', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-right"', 'Over Right Arrow'); -LatexCmds.overleftarrow = bind(Style, '\\overleftarrow', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-left"', 'Over Left Arrow'); -LatexCmds.overleftrightarrow = bind(Style, '\\overleftrightarrow ', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-leftright"', 'Over Left and Right Arrow'); -LatexCmds.overarc = bind(Style, '\\overarc', 'span', 'class="mq-non-leaf mq-overarc"', 'Over Arc'); -LatexCmds.dot = P(MathCommand, function(_, super_) { - _.init = function() { - super_.init.call(this, '\\dot', '' +LatexCmds.underline = () => new Style('\\underline', 'span', 'class="mq-non-leaf mq-underline"', 'Underline'); +LatexCmds.overline = LatexCmds.bar = () => new Style('\\overline', 'span', 'class="mq-non-leaf mq-overline"', 'Overline'); +LatexCmds.overrightarrow = () => new Style('\\overrightarrow', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-right"', 'Over Right Arrow'); +LatexCmds.overleftarrow = () => new Style('\\overleftarrow', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-left"', 'Over Left Arrow'); +LatexCmds.overleftrightarrow = () => new Style('\\overleftrightarrow ', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-leftright"', 'Over Left and Right Arrow'); +LatexCmds.overarc = () => new Style('\\overarc', 'span', 'class="mq-non-leaf mq-overarc"', 'Over Arc'); +LatexCmds.dot = () => { + return new MathCommand('\\dot', '' + '˙' + '&0' + '' ); - }; -}); +}; + // `\textcolor{color}{math}` will apply a color to the given math content, where // `color` is any valid CSS Color Value (see [SitePoint docs][] (recommended), @@ -139,19 +141,21 @@ LatexCmds.dot = P(MathCommand, function(_, super_) { // [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) { +LatexCmds.textcolor = class extends MathCommand { + color:string | undefined; + + setColor (color:string) { this.color = color; this.htmlTemplate = '&0'; - _.ariaLabel = color.replace(/^\\/, ''); - _.mathspeakTemplate = ['Start ' + _.ariaLabel + ',', 'End ' + _.ariaLabel]; + this.ariaLabel = color.replace(/^\\/, ''); + this.mathspeakTemplate = ['Start ' + this.ariaLabel + ',', 'End ' + this.ariaLabel]; }; - _.latex = function() { - return '\\textcolor{' + this.color + '}{' + this.blocks[0].latex() + '}'; + latex () { + var blocks0 = this.blocks![0]; + return '\\textcolor{' + this.color + '}{' + blocks0.latex() + '}'; }; - _.parser = function() { - var self = this; + parser () { var optWhitespace = Parser.optWhitespace; var string = Parser.string; var regex = Parser.regex; @@ -160,44 +164,47 @@ var TextColor = LatexCmds.textcolor = P(MathCommand, function(_, super_) { .then(string('{')) .then(regex(/^[#\w\s.,()%-]*/)) .skip(string('}')) - .then(function(color) { - self.setColor(color); - return super_.parser.call(self); + .then((color) => { + this.setColor(color); + return super.parser(); }) ; }; - _.isStyleBlock = function() { + isStyleBlock () { 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; +var Class = LatexCmds['class'] = class extends MathCommand { + cls:string | undefined; + + parser () { + var 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 = '&0'; - self.ariaLabel = cls + ' class'; - self.mathspeakTemplate = ['Start ' + self.ariaLabel + ',', 'End ' + self.ariaLabel]; - return super_.parser.call(self); + .then((cls) => { + this.cls = cls || ''; + this.htmlTemplate = '&0'; + this.ariaLabel = cls + ' class'; + this.mathspeakTemplate = ['Start ' + this.ariaLabel + ',', 'End ' + this.ariaLabel]; + return super.parser(); }) ; }; - _.latex = function() { - return '\\class{' + this.cls + '}{' + this.blocks[0].latex() + '}'; + latex () { + var blocks0 = this.blocks![0]; + return '\\class{' + this.cls + '}{' + blocks0.latex() + '}'; }; - _.isStyleBlock = function() { + isStyleBlock () { return true; }; -}); +}; // This test is used to determine whether an item may be treated as a whole number // for shortening the verbalized (mathspeak) forms of some fractions and superscripts. @@ -206,28 +213,32 @@ var intRgx = /^[\+\-]?[\d]+$/; // Traverses the top level of the passed block's children and returns the concatenation of their ctrlSeq properties. // Used in shortened mathspeak computations as a block's .text() method can be potentially expensive. // -function getCtrlSeqsFromBlock(block) { - if ( - typeof(block) !== 'object' || - typeof(block.children) !== 'function' - ) - return block; +function getCtrlSeqsFromBlock(block:NodeRef):string { + if (!block) return ''; + var children = block.children(); - if (!children || !children.ends[L]) return block; + if (!children || !children.ends[L]) return ''; + var chars = ''; - for (var sibling = children.ends[L]; sibling[R] !== undefined; sibling = sibling[R]) { + for (var sibling:NodeRef | undefined = children.ends[L]; sibling && sibling[R] !== undefined; sibling = sibling[R]) { if (sibling.ctrlSeq !== undefined) chars += sibling.ctrlSeq; } return chars; } -var SupSub = P(MathCommand, function(_, super_) { - _.ctrlSeq = '_{...}^{...}'; - _.createLeftOf = function(cursor) { +Options.prototype.charsThatBreakOutOfSupSub = ''; + +class SupSub extends MathCommand { + ctrlSeq = '_{...}^{...}'; + sub?:MathBlock; + sup?:MathBlock; + supsub: 'sup' | 'sub'; + + createLeftOf (cursor:Cursor) { if (!this.replacedFragment && !cursor[L] && cursor.options.supSubsRequireOperand) return; - return super_.createLeftOf.apply(this, arguments); + return super.createLeftOf(cursor); }; - _.contactWeld = function(cursor) { + contactWeld (cursor: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 @@ -238,174 +249,192 @@ var SupSub = P(MathCommand, function(_, super_) { // TODO: simplify // equiv. to [L, R].forEach(function(dir) { ... }); - for (var dir = L; dir; dir = (dir === L ? R : false)) { - if (this[dir] instanceof SupSub) { + for (var dir:L|R|false = L; dir; dir = (dir === L ? R : false)) { + const thisDir = this[dir]; + let pt; + if (thisDir 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]; + for (var supsub:'sub'|'sup'|false = 'sub'; supsub; supsub = (supsub === 'sub' ? 'sup' : false)) { + var src = this[supsub], dest = thisDir[supsub]; if (!src) continue; - if (!dest) this[dir].addBlock(src.disown()); + if (!dest) thisDir.addBlock(src.disown()); else if (!src.isEmpty()) { // ins src children at -dir end of dest - src.jQ.children().insAtDirEnd(-dir, dest.jQ); + src.jQ.children().insAtDirEnd(-dir as Direction, dest.jQ); var children = src.children().disown(); - var pt = Point(dest, children.ends[R], dest.ends[L]); + pt = new 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]); + else { + pt = new Point(dest, 0, dest.ends[L]); + } this.placeCursor = (function(dest, src) { // TODO: don't monkey-patch - return function(cursor) { cursor.insAtDirEnd(-dir, dest || src); }; + return function(cursor:Cursor) { cursor.insAtDirEnd(-dir as Direction, 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); + if (pt[L]) { + cursor.insRightOf(pt[L] as MQNode) + } else{ + cursor.insAtLeftEnd(pt.parent); + } } - else cursor.insRightOf(this[dir]); + else cursor.insRightOf(thisDir); } break; } } }; - Options.p.charsThatBreakOutOfSupSub = ''; - _.finalizeTree = function() { - this.ends[L].write = function(cursor, ch) { - if (cursor.options.autoSubscriptNumerals && this === this.parent.sub) { + finalizeTree () { + var endsL = this.ends[L] as MQNode; + endsL.write = function(cursor:Cursor, ch:string) { + if (cursor.options.autoSubscriptNumerals && this === (this.parent as SupSub).sub) { if (ch === '_') return; var cmd = this.chToCmd(ch, cursor.options); - if (cmd instanceof Symbol) cursor.deleteSelection(); + if (cmd instanceof MQSymbol) cursor.deleteSelection(); else cursor.clearSelection().insRightOf(this.parent); cmd.createLeftOf(cursor.show()); - aria.queue('Baseline').alert(cmd.mathspeak({ createdLeftOf: cursor })); + cursor.controller.aria.queue('Baseline').alert(cmd.mathspeak({ createdLeftOf: cursor })); return; } if (cursor[L] && !cursor[R] && !cursor.selection && cursor.options.charsThatBreakOutOfSupSub.indexOf(ch) > -1) { cursor.insRightOf(this.parent); - aria.queue('Baseline'); + cursor.controller.aria.queue('Baseline'); } - MathBlock.p.write.apply(this, arguments); + MathBlock.prototype.write.call(this, cursor, ch); }; }; - _.moveTowards = function(dir, cursor, updown) { + moveTowards (dir:Direction, cursor:Cursor, updown?:'up'|'down') { if (cursor.options.autoSubscriptNumerals && !this.sup) { cursor.insDirOf(dir, this); } - else super_.moveTowards.apply(this, arguments); + else super.moveTowards(dir, cursor, updown); }; - _.deleteTowards = function(dir, cursor) { + deleteTowards (dir:Direction, cursor: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)); + var cmd = this.sub.ends[-dir as Direction]; + if (cmd instanceof MQSymbol) cmd.remove(); + else if (cmd) cmd.deleteTowards(dir, cursor.insAtDirEnd(-dir as Direction, 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); + if (this.sup) cursor.insDirOf(-dir as Direction, 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); + else super.deleteTowards(dir, cursor) }; - _.latex = function() { - function latex(prefix, block) { + latex () { + function latex(prefix:string, block:NodeRef | undefined) { var l = block && block.latex(); return block ? prefix + '{' + (l || ' ') + '}' : ''; } return latex('_', this.sub) + latex('^', this.sup); }; - _.text = function() { - function text(prefix, block) { - var l = block && block.text(); + text () { + function text(prefix:string, block:NodeRef | undefined) { + var l = (block && block.text()) || ''; return block ? prefix + (l.length === 1 ? l : '(' + (l || ' ') + ')') : ''; } return text('_', this.sub) + text('^', this.sup); }; - _.addBlock = function(block) { + addBlock (block:MathBlock) { if (this.supsub === 'sub') { - this.sup = this.upInto = this.sub.upOutOf = block; - block.adopt(this, this.sub, 0).downOutOf = this.sub; + this.sup = this.upInto = (this.sub as MQNode).upOutOf = block; + block.adopt(this, (this.sub as MQNode), 0).downOutOf = this.sub; block.jQ = $('').append(block.jQ.children()).prependTo(this.jQ); - Node.linkElementByBlockNode(block.jQ[0], block); + NodeBase.linkElementByBlockNode(block.jQ[0], block); } else { - this.sub = this.downInto = this.sup.downOutOf = block; - block.adopt(this, 0, this.sup).upOutOf = this.sup; + this.sub = this.downInto = (this.sup as MQNode).downOutOf = block; + block.adopt(this, 0, (this.sup as MQNode)).upOutOf = this.sup; block.jQ = $('').append(block.jQ.children()) .appendTo(this.jQ.removeClass('mq-sup-only')); - Node.linkElementByBlockNode(block.jQ[0], block); + NodeBase.linkElementByBlockNode(block.jQ[0], block); this.jQ.append(''); } // 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); + for (var i = 0; i < 2; i += 1) (function(cmd:SupSub, supsub:'sup'|'sub', oppositeSupsub:'sup'|'sub', updown:'up'|'down') { + const cmdSubSub = cmd[supsub]!; + cmdSubSub.deleteOutOf = function(dir:Direction, cursor:Cursor) { + cursor.insDirOf((this[dir] ? (-dir as Direction) : 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; + .withDirAdopt(dir, cursor.parent, cursor[dir] as MQNode, cursor[-dir as Direction] as NodeRef) + .jQ.insDirOf(-dir as Direction, cursor.jQ); + cursor[-dir as Direction] = end; } cmd.supsub = oppositeSupsub; delete cmd[supsub]; - delete cmd[updown+'Into']; - cmd[oppositeSupsub][updown+'OutOf'] = insLeftOfMeUnlessAtEnd; - delete cmd[oppositeSupsub].deleteOutOf; + delete cmd[`${updown}Into`]; + const cmdOppositeSupsub = cmd[oppositeSupsub]!; + cmdOppositeSupsub[`${updown}OutOf`] = insLeftOfMeUnlessAtEnd; + delete (cmdOppositeSupsub as any).deleteOutOf; // TODO - refactor so this method can be optional 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])); + }(this, 'sub sup'.split(' ')[i] as 'sup'|'sup', 'sup sub'.split(' ')[i] as 'sup'|'sup', 'down up'.split(' ')[i] as 'up' | 'down')); }; -}); +}; -function insLeftOfMeUnlessAtEnd(cursor) { +function insLeftOfMeUnlessAtEnd(this:MQNode, cursor: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; + var cmd = this.parent; + var ancestorCmd:MQNode|Anticursor|Cursor = cursor; do { if (ancestorCmd[R]) return cursor.insLeftOf(cmd); ancestorCmd = ancestorCmd.parent.parent; } while (ancestorCmd !== cmd); cursor.insRightOf(cmd); + return undefined; } -LatexCmds.subscript = -LatexCmds._ = P(SupSub, function(_, super_) { - _.supsub = 'sub'; - _.htmlTemplate = +class SubscriptCommand extends SupSub { + supsub = 'sub' as const; + + htmlTemplate = '' + '&0' + '' + '' - ; - _.textTemplate = [ '_' ]; - _.mathspeakTemplate = [ 'Subscript,', ', Baseline']; - _.ariaLabel = 'subscript'; - _.finalizeTree = function() { - this.downInto = this.sub = this.ends[L]; + + textTemplate = [ '_' ]; + + mathspeakTemplate = [ 'Subscript,', ', Baseline']; + + ariaLabel = 'subscript'; + + finalizeTree () { + this.downInto = this.sub = this.ends[L] as MathBlock; this.sub.upOutOf = insLeftOfMeUnlessAtEnd; - super_.finalizeTree.call(this); + super.finalizeTree() }; -}); +}; +LatexCmds.subscript = +LatexCmds._ = SubscriptCommand; LatexCmds.superscript = LatexCmds.supscript = -LatexCmds['^'] = P(SupSub, function(_, super_) { - _.supsub = 'sup'; - _.htmlTemplate = +LatexCmds['^'] = class SuperscriptCommand extends SupSub { + supsub = 'sup' as const; + + htmlTemplate = '' + '&0' + '' ; - _.textTemplate = ['^(', ')']; - _.mathspeak = function(opts) { + textTemplate = ['^(', ')']; + mathspeak (opts?:MathspeakOptions) { // Simplify basic exponent speech for common whole numbers. var child = this.upInto; if (child !== undefined) { @@ -447,21 +476,24 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { return 'to the ' + innerMathspeak + suffix + ' power'; } } - return super_.mathspeak.call(this); + return super.mathspeak(); }; - _.ariaLabel = 'superscript'; - _.mathspeakTemplate = [ 'Superscript,', ', Baseline']; - _.finalizeTree = function() { - this.upInto = this.sup = this.ends[R]; + ariaLabel = 'superscript'; + mathspeakTemplate = [ 'Superscript,', ', Baseline']; + finalizeTree () { + this.upInto = this.sup = this.ends[R] as MathBlock; this.sup.downOutOf = insLeftOfMeUnlessAtEnd; - super_.finalizeTree.call(this); + super.finalizeTree(); }; -}); +}; -var SummationNotation = P(MathCommand, function(_, super_) { - _.init = function(ch, html, ariaLabel) { - _.ariaLabel = ariaLabel || ctrlSeq.replace(/^\\/, ''); +class SummationNotation extends MathCommand { + + constructor (ch:string, html:string, ariaLabel?:string) { + super(); + + this.ariaLabel = ariaLabel || ch.replace(/^\\/, ''); var htmlTemplate = '' + '&1' @@ -469,34 +501,35 @@ var SummationNotation = P(MathCommand, function(_, super_) { + '&0' + '' ; - Symbol.prototype.init.call(this, ch, htmlTemplate); + + MQSymbol.prototype.setCtrlSeqHtmlTextAndMathspeak.call(this, ch, htmlTemplate); }; - _.createLeftOf = function(cursor) { - super_.createLeftOf.apply(this, arguments); + createLeftOf (cursor:Cursor) { + super.createLeftOf(cursor); if (cursor.options.sumStartsWithNEquals) { - Letter('n').createLeftOf(cursor); - Equality().createLeftOf(cursor); + new Letter('n').createLeftOf(cursor); + new Equality().createLeftOf(cursor); } }; - _.latex = function() { - function simplify(latex) { + latex () { + function simplify(latex:string) { return '{' + (latex || ' ') + '}'; } - return this.ctrlSeq + '_' + simplify(this.ends[L].latex()) + - '^' + simplify(this.ends[R].latex()); + return this.ctrlSeq + '_' + simplify((this.ends[L] as MQNode).latex()) + + '^' + simplify((this.ends[R] as MQNode).latex()); }; - _.mathspeak = function() { - return 'Start ' + this.ariaLabel + ' from ' + this.ends[L].mathspeak() + - ' to ' + this.ends[R].mathspeak() + ', end ' + this.ariaLabel + ', '; + mathspeak () { + return 'Start ' + this.ariaLabel + ' from ' + (this.ends[L] as MQNode).mathspeak() + + ' to ' + (this.ends[R] as MQNode).mathspeak() + ', end ' + this.ariaLabel + ', '; }; - _.parser = function() { + parser () { 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() ]; + var blocks = self.blocks = [ new MathBlock(), new MathBlock() ]; for (var i = 0; i < blocks.length; i += 1) { blocks[i].adopt(self, self.ends[R], 0); } @@ -509,32 +542,34 @@ var SummationNotation = P(MathCommand, function(_, super_) { }); }).many().result(self); }; - _.finalizeTree = function() { - this.ends[L].ariaLabel = 'lower bound'; - this.ends[R].ariaLabel = 'upper bound'; - this.downInto = this.ends[L]; - this.upInto = this.ends[R]; - this.ends[L].upOutOf = this.ends[R]; - this.ends[R].downOutOf = this.ends[L]; + finalizeTree () { + var endsL = this.ends[L] as MQNode; + var endsR = this.ends[R] as MQNode; + + endsL.ariaLabel = 'lower bound'; + endsR.ariaLabel = 'upper bound'; + this.downInto = endsL + this.upInto = endsR; + endsL.upOutOf = endsR; + endsR.downOutOf = endsL; }; -}); +}; LatexCmds['∑'] = LatexCmds.sum = -LatexCmds.summation = bind(SummationNotation,'\\sum ','∑', 'sum'); +LatexCmds.summation = () => new SummationNotation('\\sum ','∑', 'sum'); LatexCmds['∏'] = LatexCmds.prod = -LatexCmds.product = bind(SummationNotation,'\\prod ','∏', 'product'); +LatexCmds.product = () => new SummationNotation('\\prod ','∏', 'product'); LatexCmds.coprod = -LatexCmds.coproduct = bind(SummationNotation,'\\coprod ','∐', 'co product'); +LatexCmds.coproduct = () => new SummationNotation('\\coprod ','∐', 'co product'); LatexCmds['∫'] = LatexCmds['int'] = -LatexCmds.integral = P(SummationNotation, function(_, super_) { - _.init = function() { - _.ariaLabel = 'integral'; +LatexCmds.integral = class extends SummationNotation { + constructor () { var htmlTemplate = '' + '' @@ -545,30 +580,39 @@ LatexCmds.integral = P(SummationNotation, function(_, super_) { + '' + '' ; - Symbol.prototype.init.call(this, '\\int ', htmlTemplate, 'integral'); + + super('\\int ', '', 'integral'); + + this.ariaLabel = 'integral'; + this.htmlTemplate = htmlTemplate; }; - // FIXME: refactor rather than overriding - _.createLeftOf = MathCommand.p.createLeftOf; -}); + + createLeftOf (cursor:Cursor) { + // FIXME: refactor rather than overriding + MathCommand.prototype.createLeftOf.call(this, cursor); + } +}; var Fraction = LatexCmds.frac = LatexCmds.dfrac = LatexCmds.cfrac = -LatexCmds.fraction = P(MathCommand, function(_, super_) { - _.ctrlSeq = '\\frac'; - _.htmlTemplate = +LatexCmds.fraction = class FracNode extends MathCommand { + ctrlSeq = '\\frac'; + htmlTemplate = '' + '&0' + '&1' + '' + '' ; - _.textTemplate = ['(', ')/(', ')']; - _.finalizeTree = function() { - this.upInto = this.ends[R].upOutOf = this.ends[L]; - this.downInto = this.ends[L].downOutOf = this.ends[R]; - this.ends[L].ariaLabel = 'numerator'; - this.ends[R].ariaLabel = 'denominator'; + textTemplate = ['(', ')/(', ')']; + finalizeTree () { + const endsL = this.ends[L] as MQNode; + const endsR = this.ends[R] as MQNode; + this.upInto = endsR.upOutOf = endsL; + this.downInto = endsL.downOutOf = endsR; + endsL.ariaLabel = 'numerator'; + endsR.ariaLabel = 'denominator'; if(this.getFracDepth() > 1) { this.mathspeakTemplate = ['StartNestedFraction,', 'NestedOver', ', EndNestedFraction']; } else { @@ -576,7 +620,7 @@ LatexCmds.fraction = P(MathCommand, function(_, super_) { } }; - _.mathspeak = function(opts) { + mathspeak (opts?:MathspeakOptions) { if (opts && opts.createdLeftOf) { var cursor = opts.createdLeftOf; return cursor.parent.mathspeak(); @@ -631,11 +675,11 @@ LatexCmds.fraction = P(MathCommand, function(_, super_) { // Such combinations should be spoken aloud as "1 and 1 half." // Start at the left sibling of the fraction and continue leftward until something other than a digit or whitespace is found. var precededByInteger = false; - for (var sibling = this[L]; sibling[L] !== undefined; sibling = sibling[L]) { + for (var sibling:NodeRef | undefined = this[L]; sibling && sibling[L] !== undefined; sibling = sibling[L]) { // Ignore whitespace if (sibling.ctrlSeq === '\\ ') { continue; - } else if (intRgx.test(sibling.ctrlSeq)) { + } else if (intRgx.test(sibling.ctrlSeq || '')) { precededByInteger = true; } else { precededByInteger = false; @@ -645,29 +689,29 @@ LatexCmds.fraction = P(MathCommand, function(_, super_) { if (precededByInteger) { output += 'and '; } - output += this.ends[L].mathspeak() + ' ' + newDenSpeech; + output += (this.ends[L] as MQNode).mathspeak() + ' ' + newDenSpeech; return output; } } - return super_.mathspeak.apply(this, arguments); + return super.mathspeak(); }; - _.getFracDepth = function() { + getFracDepth () { var level = 0; - var walkUp = function(item, level) { - if(item instanceof Node && item.ctrlSeq && item.ctrlSeq.toLowerCase().search('frac') >= 0) level += 1; - if(item.parent) return walkUp(item.parent, level); + var walkUp = function(item:NodeRef, level:number):number { + if(item instanceof MQNode && item.ctrlSeq && item.ctrlSeq.toLowerCase().search('frac') >= 0) level += 1; + if(item && item.parent) return walkUp(item.parent, level); else return level; }; return walkUp(this, level); }; -}); +}; var LiveFraction = LatexCmds.over = -CharCmds['/'] = P(Fraction, function(_, super_) { - _.createLeftOf = function(cursor) { +CharCmds['/'] = class extends Fraction { + createLeftOf (cursor:Cursor) { if (!this.replacedFragment) { var leftward = cursor[L]; @@ -678,51 +722,47 @@ CharCmds['/'] = P(Fraction, function(_, super_) { leftward instanceof (LatexCmds.text || noop) || leftward instanceof SummationNotation || leftward.ctrlSeq === '\\ ' || - /^[,;:]$/.test(leftward.ctrlSeq) + /^[,;:]$/.test(leftward.ctrlSeq as string) ) //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] as MQNode; + let leftwardR = leftward[R]; + if (leftwardR instanceof SupSub && leftwardR.ctrlSeq != leftward.ctrlSeq) leftward = leftward[R]; } if (leftward !== cursor[L] && !cursor.isTooDeep(1)) { - this.replaces(Fragment(leftward[R] || cursor.parent.ends[L], cursor[L])); + let leftwardR = (leftward as MQNode)[R] as MQNode; + let cursorL = cursor[L] as MQNode; + + this.replaces(new Fragment(leftwardR || cursor.parent.ends[L], cursorL)); cursor[L] = leftward; } } - super_.createLeftOf.call(this, cursor); + super.createLeftOf(cursor); }; -}); +}; -LatexCmds.ans = P(Symbol, function(_, super_) { - _.init = function(ch) { - super_.init.call(this, +const AnsBuilder = () => new MQSymbol( '\\operatorname{ans}', 'ans', 'ans' ); - }; -}); +LatexCmds.ans = AnsBuilder; -LatexCmds.percent = -LatexCmds.percentof = P(Symbol, function (_, super_) { - _.init = function () { - super_.init.call( - this, +const PercentOfBuilder = () => new MQSymbol( '\\%\\operatorname{of}', '% of ', 'percent of' ) - }; -}); +LatexCmds.percent = +LatexCmds.percentof = PercentOfBuilder -var SquareRoot = -LatexCmds.sqrt = P(MathCommand, function(_, super_) { - _.ctrlSeq = '\\sqrt'; - _.htmlTemplate = +class SquareRoot extends MathCommand { + ctrlSeq = '\\sqrt'; + htmlTemplate = '' + '' + SVG_SYMBOLS.sqrt.html @@ -730,36 +770,36 @@ LatexCmds.sqrt = P(MathCommand, function(_, super_) { + '&0' + '' ; - _.textTemplate = ['sqrt(', ')']; - _.mathspeakTemplate = ['StartRoot,', ', EndRoot']; - _.ariaLabel = 'root'; - _.parser = function() { + textTemplate = ['sqrt(', ')']; + mathspeakTemplate = ['StartRoot,', ', EndRoot']; + ariaLabel = 'root'; + parser () { return latexMathParser.optBlock.then(function(optBlock) { return latexMathParser.block.map(function(block) { - var nthroot = NthRoot(); + var nthroot = new NthRoot(); nthroot.blocks = [ optBlock, block ]; optBlock.adopt(nthroot, 0, 0); block.adopt(nthroot, optBlock, 0); return nthroot; }); - }).or(super_.parser.call(this)); + }).or(super.parser()); }; -}); +}; +LatexCmds.sqrt = SquareRoot; -var Hat = LatexCmds.hat = P(MathCommand, function(_, super_) { - _.ctrlSeq = '\\hat'; - _.htmlTemplate = +LatexCmds.hat = class Hat extends MathCommand { + ctrlSeq = '\\hat'; + htmlTemplate = '' + '^' + '&0' + '' ; - _.textTemplate = ['hat(', ')']; -}); + textTemplate = ['hat(', ')']; +}; -var NthRoot = -LatexCmds.nthroot = P(SquareRoot, function(_, super_) { - _.htmlTemplate = +class NthRoot extends SquareRoot { + htmlTemplate = '' + '&0' + '' @@ -770,34 +810,34 @@ LatexCmds.nthroot = P(SquareRoot, function(_, super_) { + '' + '' ; - _.textTemplate = ['sqrt[', '](', ')']; - _.latex = function() { - return '\\sqrt['+this.ends[L].latex()+']{'+this.ends[R].latex()+'}'; + textTemplate = ['sqrt[', '](', ')']; + latex () { + return '\\sqrt['+(this.ends[L] as MQNode).latex()+']{'+(this.ends[R] as MQNode).latex()+'}'; }; - _.mathspeak = function() { - var indexMathspeak = this.ends[L].mathspeak(); - var radicandMathspeak = this.ends[R].mathspeak(); - this.ends[L].ariaLabel = 'Index'; - this.ends[R].ariaLabel = 'Radicand'; + mathspeak () { + var indexMathspeak = (this.ends[L] as MQNode).mathspeak(); + var radicandMathspeak = (this.ends[R] as MQNode).mathspeak(); + (this.ends[L] as MQNode).ariaLabel = 'Index'; + (this.ends[R] as MQNode).ariaLabel = 'Radicand'; if (indexMathspeak === '3') { // cube root return 'Start Cube Root, '+radicandMathspeak+', End Cube Root'; } else { return 'Root Index '+indexMathspeak+', Start Root, '+radicandMathspeak+', End Root'; } }; -}); +}; +LatexCmds.nthroot = NthRoot; -var CubeRoot = -LatexCmds.cbrt = P(NthRoot, function(_, super_) { - _.createLeftOf = function(cursor) { - super_.createLeftOf.apply(this, arguments); - Digit('3').createLeftOf(cursor); +LatexCmds.cbrt = class extends NthRoot { + createLeftOf (cursor:Cursor) { + super.createLeftOf(cursor); + new Digit('3').createLeftOf(cursor); cursor.controller.moveRight(); }; -}); +}; -var DiacriticAbove = P(MathCommand, function(_, super_) { - _.init = function(ctrlSeq, symbol, textTemplate) { +class DiacriticAbove extends MathCommand { + constructor (ctrlSeq:string, symbol:string, textTemplate?:string[]) { var htmlTemplate = '' + ''+symbol+'' @@ -805,33 +845,43 @@ var DiacriticAbove = P(MathCommand, function(_, super_) { + '' ; - super_.init.call(this, ctrlSeq, htmlTemplate, textTemplate); + super(ctrlSeq, htmlTemplate, textTemplate); }; -}); -LatexCmds.vec = bind(DiacriticAbove, '\\vec', '→', ['vec(', ')']); -LatexCmds.tilde = bind(DiacriticAbove, '\\tilde', '~', ['tilde(', ')']); +}; +LatexCmds.vec = () => new DiacriticAbove('\\vec', '→', ['vec(', ')']); +LatexCmds.tilde = () => new DiacriticAbove('\\tilde', '~', ['tilde(', ')']); -function DelimsMixin(_, super_) { - _.jQadd = function() { - super_.jQadd.apply(this, arguments); +class DelimsNode extends MathCommand { + delimjQs:$; + contentjQ:$; + + jQadd (el:$) { + super.jQadd(el); this.delimjQs = this.jQ.children(':first').add(this.jQ.children(':last')); this.contentjQ = this.jQ.children(':eq(1)'); + return this.jQ; }; } // 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]); +class Bracket extends DelimsNode { + side: BracketSide; + sides:{ + [L]: {ch:string, ctrlSeq:string}, + [R]: {ch:string, ctrlSeq:string} + } + constructor (side:BracketSide, open:string, close:string, ctrlSeq:string, end:string) { + super('\\left'+ctrlSeq, undefined, [open, close]); this.side = side; - this.sides = {}; - this.sides[L] = { ch: open, ctrlSeq: ctrlSeq }; - this.sides[R] = { ch: close, ctrlSeq: end }; + this.sides = { + [L]: { ch: open, ctrlSeq: ctrlSeq }, + [R]: { ch: close, ctrlSeq: end } + }; }; - _.numBlocks = function() { return 1; }; - _.html = function() { + numBlocks () { return 1; }; + html () { var leftSymbol = this.getSymbol(L); var rightSymbol = this.getSymbol(R); @@ -847,15 +897,16 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) { + '' + '' ; - return super_.html.call(this); + return super.html(); }; - _.getSymbol = function (side) { - return SVG_SYMBOLS[this.sides[side || R].ch] || {width: '0', html: ''}; + getSymbol (side:BracketSide) { + var ch = (this.sides[side || R].ch) as keyof typeof SVG_SYMBOLS; + return SVG_SYMBOLS[ch] || {width: '0', html: ''}; }; - _.latex = function() { - return '\\left'+this.sides[L].ctrlSeq+this.ends[L].latex()+'\\right'+this.sides[R].ctrlSeq; + latex () { + return '\\left'+this.sides[L].ctrlSeq+(this.ends[L] as MQNode).latex()+'\\right'+this.sides[R].ctrlSeq; }; - _.mathspeak = function(opts) { + mathspeak (opts?:MathspeakOptions) { var open = this.sides[L].ch, close = this.sides[R].ch; if (open === '|' && close === '|') { this.mathspeakTemplate = ['StartAbsoluteValue,', ', EndAbsoluteValue']; @@ -865,119 +916,125 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) { var ch = ''; if (this.side === L) ch = this.textTemplate[0]; else if (this.side === R) ch = this.textTemplate[1]; - return (this.side === L ? 'left ' : 'right ') + BRACKET_NAMES[ch]; + return (this.side === L ? 'left ' : 'right ') + BRACKET_NAMES[ch as keyof typeof BRACKET_NAMES]; } else { - this.mathspeakTemplate = ['left ' + BRACKET_NAMES[open]+',', ', right ' + BRACKET_NAMES[close]]; - this.ariaLabel = BRACKET_NAMES[open]+' block'; + this.mathspeakTemplate = ['left ' + BRACKET_NAMES[open as keyof typeof BRACKET_NAMES]+',', ', right ' + BRACKET_NAMES[close as keyof typeof BRACKET_NAMES]]; + this.ariaLabel = BRACKET_NAMES[open as keyof typeof BRACKET_NAMES]+' block'; } - return super_.mathspeak.call(this); + return super.mathspeak(); }; - _.matchBrack = function(opts, expectedSide, node) { + matchBrack (opts:CursorOptions, expectedSide:BracketSide, node:NodeRef | undefined) { // 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 + || OPP_BRACKS[this.sides[this.side as Direction].ch as keyof typeof BRACKET_NAMES] === node.sides[node.side].ch || { '(': ']', '[': ')' }[this.sides[L].ch] === node.sides[R].ch) && node; }; - _.closeOpposing = function(brack) { + closeOpposing (brack:Bracket) { brack.side = 0; - brack.sides[this.side] = this.sides[this.side]; // copy over my info (may be + brack.sides[this.side as Direction] = this.sides[this.side as Direction]; // copy over my info (may be var $brack = brack.delimjQs.eq(this.side === L ? 0 : 1) // mismatched, like [a, b)) .removeClass('mq-ghost'); this.replaceBracket($brack, this.side); }; - _.createLeftOf = function(cursor) { + createLeftOf (cursor:Cursor) { + var brack; 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); + 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); + brack = this.matchBrack(opts, -this.side as BracketSide, cursor[-this.side as Direction]) + || this.matchBrack(opts, -this.side as BracketSide, cursor.parent.parent); } } if (brack) { - var side = this.side = -brack.side; // may be pipe with .side not yet set + var side = this.side = -brack.side as BracketSide; // 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); + if (brack === cursor.parent.parent && cursor[side as Direction]) { // move the stuff between + new Fragment(cursor[side as Direction] as MQNode, cursor.parent.ends[side as Direction] as MQNode, -side as Direction) // me and ghost outside + .disown().withDirAdopt(-side as Direction, brack.parent, brack, brack[side as Direction] as MQNode) + .jQ.insDirOf(side as Direction, brack.jQ); } - brack.bubble(function (node) { node.reflow(); }); + brack.bubble(function (node) { node.reflow(); return undefined; }); } 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; + else if (cursor[-side as Direction]) { // elsewise, auto-expand so ghost is at far end + brack.replaces(new Fragment(cursor[-side as Direction] as MQNode, cursor.parent.ends[-side as Direction] as MQNode, side as Direction)); + cursor[-side as Direction] = 0; } - super_.createLeftOf.call(brack, cursor); + super.createLeftOf(cursor); } - if (side === L) cursor.insAtLeftEnd(brack.ends[L]); + if (side === L) cursor.insAtLeftEnd(brack.ends[L] as MQNode); else cursor.insRightOf(brack); }; - _.placeCursor = noop; - _.unwrap = function() { - this.ends[L].children().disown().adopt(this.parent, this, this[R]) + placeCursor () {}; + unwrap () { + (this.ends[L] as MQNode).children().disown().adopt(this.parent, this, this[R]) .jQ.insertAfter(this.jQ); this.remove(); }; - _.deleteSide = function(side, outward, cursor) { + deleteSide (side:Direction, outward:boolean, cursor: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); + sib ? cursor.insDirOf(-side as Direction, sib) : cursor.insAtDirEnd(side, parent); return; } var opts = cursor.options, wasSolid = !this.side; - this.side = -side; + this.side = -side as Direction; // 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]; + if (this.matchBrack(opts, side, (this.ends[L] as MQNode).ends[this.side])) { // is ghost, + this.closeOpposing((this.ends[L] as MQNode).ends[this.side as Direction] as Bracket); // then become [1+2)+3 + var origEnd = (this.ends[L] as MQNode).ends[side]; this.unwrap(); - if (origEnd.siblingCreated) origEnd.siblingCreated(cursor.options, side); - sib ? cursor.insDirOf(-side, sib) : cursor.insAtDirEnd(side, parent); + if (origEnd) origEnd.siblingCreated(cursor.options, side); + if (sib) { + cursor.insDirOf(-side as Direction, sib) + } else { + 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(); + + (this.parent.parent as Bracket).closeOpposing(this); // ghost, then become [1+2+3) + (this.parent.parent as Bracket).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); + sib ? cursor.insDirOf(-side as Direction, 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.sides[side] = getOppBracketSide(this); var $brack = this.delimjQs.removeClass('mq-ghost') .eq(side === L ? 0 : 1).addClass('mq-ghost'); this.replaceBracket($brack, side); } 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); + var origEnd = (this.ends[L] as MQNode).ends[side]; + new Fragment(sib, farEnd as MQNode, -side as Direction).disown() + .withDirAdopt(-side as Direction, this.ends[L] as MQNode, origEnd as MQNode, 0) + .jQ.insAtDirEnd(side, (this.ends[L] as MQNode).jQ.removeClass('mq-empty')); + if (origEnd) origEnd.siblingCreated(cursor.options, side); + cursor.insDirOf(-side as Direction, 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])); + : cursor.insAtDirEnd(side, this.ends[L] as MQNode)); } }; - _.replaceBracket = function ($brack, side) { + replaceBracket ($brack:$, side:BracketSide) { var symbol = this.getSymbol(side); $brack.html(symbol.html).css('width', symbol.width); @@ -987,12 +1044,12 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) { $brack.prev().css('margin-right', symbol.width); } }; - _.deleteTowards = function(dir, cursor) { - this.deleteSide(-dir, false, cursor); + deleteTowards (dir:Direction, cursor:Cursor) { + this.deleteSide(-dir as Direction, false, cursor); }; - _.finalizeTree = function() { - this.ends[L].deleteOutOf = function(dir, cursor) { - this.parent.deleteSide(dir, true, cursor); + finalizeTree () { + (this.ends[L] as MQNode).deleteOutOf = function(dir:Direction, cursor:Cursor) { + (this.parent as Bracket).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 @@ -1001,10 +1058,19 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) { this.side = 0; }; }; - _.siblingCreated = function(opts, dir) { // if something typed between ghost and far + siblingCreated (_opts:Options, dir:Direction) { // if something typed between ghost and far if (dir === -this.side) this.finalizeTree(); // end of its block, solidify }; -}); +}; + +function getOppBracketSide (bracket:Bracket) { + var side = bracket.side as Direction; + var data = bracket.sides[side]; + return { + ch: OPP_BRACKS[data.ch as keyof typeof OPP_BRACKS], + ctrlSeq: OPP_BRACKS[data.ctrlSeq as keyof typeof OPP_BRACKS] + } +} var OPP_BRACKS = { '(': ')', @@ -1030,27 +1096,28 @@ var BRACKET_NAMES = { '|': 'pipe' }; -function bindCharBracketPair(open, ctrlSeq, name) { - 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); - BRACKET_NAMES[open] = BRACKET_NAMES[close] = name; +function bindCharBracketPair(open:keyof typeof OPP_BRACKS, ctrlSeq:string, name:string) { + var ctrlSeq = ctrlSeq || open; + var close = OPP_BRACKS[open]; + var end = OPP_BRACKS[ctrlSeq as keyof typeof OPP_BRACKS]; + CharCmds[open] = () => new Bracket(L, open, close, ctrlSeq, end); + CharCmds[close] = () => new Bracket(R, open, close, ctrlSeq, end); + BRACKET_NAMES[open as keyof typeof BRACKET_NAMES] = BRACKET_NAMES[close as keyof typeof BRACKET_NAMES] = name; } -bindCharBracketPair('(', null, 'parenthesis'); -bindCharBracketPair('[', null, 'bracket'); +bindCharBracketPair('(', '', 'parenthesis'); +bindCharBracketPair('[', '', 'bracket'); bindCharBracketPair('{', '\\{', 'brace'); -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.langle = () => new Bracket(L, '⟨', '⟩', '\\langle ', '\\rangle '); +LatexCmds.rangle = () => new Bracket(R, '⟨', '⟩', '\\langle ', '\\rangle '); +CharCmds['|'] = () => new Bracket(L, '|', '|', '|', '|'); +LatexCmds.lVert = () => new Bracket(L, '∥', '∥', '\\lVert ', '\\rVert '); +LatexCmds.rVert = () => new Bracket(R, '∥', '∥', '\\lVert ', '\\rVert '); -LatexCmds.left = P(MathCommand, function(_) { - _.parser = function() { +LatexCmds.left = class extends MathCommand { + parser () { var regex = Parser.regex; var string = Parser.string; - var succeed = Parser.succeed; var optWhitespace = Parser.optWhitespace; return optWhitespace.then(regex(/^(?:[([|]|\\\{|\\langle(?![a-zA-Z])|\\lVert(?![a-zA-Z]))/)) @@ -1064,7 +1131,7 @@ LatexCmds.left = P(MathCommand, function(_) { var close = end.replace(/^\\/, ''); if (end=="\\rangle") { close = '⟩'; end = end + ' '; } if (end=="\\rVert") { close = '∥'; end = end + ' '; } - var cmd = Bracket(0, open, close, ctrlSeq, end); + var cmd = new Bracket(0, open, close, ctrlSeq, end); cmd.blocks = [ block ]; block.adopt(cmd, 0, 0); return cmd; @@ -1074,64 +1141,66 @@ LatexCmds.left = P(MathCommand, function(_) { }) ; }; -}); +}; -LatexCmds.right = P(MathCommand, function(_) { - _.parser = function() { +LatexCmds.right = class extends MathCommand { + parser () { return Parser.fail('unmatched \\right'); }; -}); +}; -var Binomial = -LatexCmds.binom = -LatexCmds.binomial = P(P(MathCommand, DelimsMixin), function(_, super_) { - var leftSymbol = SVG_SYMBOLS['(']; - var rightSymbol = SVG_SYMBOLS[')']; +var leftBinomialSymbol = SVG_SYMBOLS['(']; +var rightBinomialSymbol = SVG_SYMBOLS[')']; +class Binomial extends DelimsNode { - _.ctrlSeq = '\\binom'; - _.htmlTemplate = + ctrlSeq = '\\binom'; + htmlTemplate = '' - + '' - + leftSymbol.html + + '' + + leftBinomialSymbol.html + '' - + '' + + '' + '' + '&0' + '&1' + '' + '' - + '' - + rightSymbol.html + + '' + + rightBinomialSymbol.html + '' + '' ; - _.textTemplate = ['choose(',',',')']; - _.mathspeakTemplate = ['StartBinomial,', 'Choose', ', EndBinomial']; - _.ariaLabel = 'binomial'; -}); + textTemplate = ['choose(',',',')']; + mathspeakTemplate = ['StartBinomial,', 'Choose', ', EndBinomial']; + ariaLabel = 'binomial'; +}; -var Choose = -LatexCmds.choose = P(Binomial, function(_) { - _.createLeftOf = LiveFraction.prototype.createLeftOf; -}); +LatexCmds.binom = +LatexCmds.binomial = Binomial; -LatexCmds.editable = // backcompat with before cfd3620 on #233 -LatexCmds.MathQuillMathField = P(MathCommand, function(_, super_) { - _.ctrlSeq = '\\MathQuillMathField'; - _.htmlTemplate = +LatexCmds.choose = class extends Binomial { + createLeftOf (cursor:Cursor) { + LiveFraction.prototype.createLeftOf(cursor); + } +}; + +class MathFieldNode extends MathCommand { + name:string; + ctrlSeq = '\\MathQuillMathField'; + htmlTemplate = '' + '&0' + '' ; - _.parser = function() { + parser () { 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)); + .map(function(name) { self.name = name; }).or(succeed(undefined)) + .then(super.parser()); }; - _.finalizeTree = function(options) { - var ctrlr = Controller(this.ends[L], this.jQ, options); + finalizeTree (options:CursorOptions) { + var ctrlr = new Controller(this.ends[L] as ControllerRoot, this.jQ, options); ctrlr.KIND_OF_MQ = 'MathField'; ctrlr.editable = true; ctrlr.createTextarea(); @@ -1139,29 +1208,34 @@ LatexCmds.MathQuillMathField = P(MathCommand, function(_, super_) { ctrlr.cursor.insAtRightEnd(ctrlr.root); RootBlockMixin(ctrlr.root); }; - _.registerInnerField = function(innerFields, MathField) { - innerFields.push(innerFields[this.name] = MathField(this.ends[L].controller)); + registerInnerField (innerFields:InnerFields, MathField:InnerMathField) { + const controller = (this.ends[L] as RootMathBlock).controller; + const newField = new MathField(controller); + innerFields[this.name] = newField + innerFields.push(newField); }; - _.latex = function(){ return this.ends[L].latex(); }; - _.text = function(){ return this.ends[L].text(); }; -}); + latex (){ return (this.ends[L] as MQNode).latex(); }; + text (){ return (this.ends[L] as MQNode).text(); }; +}; +LatexCmds.editable = // backcompat with before cfd3620 on #233 +LatexCmds.MathQuillMathField = MathFieldNode; // Embed arbitrary things // Probably the closest DOM analogue would be an iframe? -// From MathQuill's perspective, it's a Symbol, it can be +// From MathQuill's perspective, it's a MQSymbol, 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) { +class EmbedNode extends MQSymbol { + setOptions (options:EmbedOptions) { function noop () { return ""; } this.text = options.text || noop; this.htmlTemplate = options.htmlString || ""; this.latex = options.latex || noop; return this; }; - _.parser = function() { + parser () { var self = this, string = Parser.string, regex = Parser.regex, succeed = Parser.succeed; return string('{').then(regex(/^[a-z][a-z0-9]*/i)).skip(string('}')) @@ -1169,11 +1243,12 @@ var Embed = LatexCmds.embed = P(Symbol, function(_, super_) { // 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) { + .or(succeed(undefined)).map(function(data) { return self.setOptions(EMBEDS[name](data)); }) ; }) ; }; -}); +}; +LatexCmds.embed = EmbedNode; diff --git a/src/commands/text.js b/src/commands/text.js deleted file mode 100644 index fdaaf5da0..000000000 --- a/src/commands/text.js +++ /dev/null @@ -1,428 +0,0 @@ -/************************************************* - * 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'; - _.ariaLabel = '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); - - cursor.insAtRightEnd(textBlock); - - if (textBlock.replacedText) - for (var i = 0; i < textBlock.replacedText.length; i += 1) - textBlock.write(cursor, textBlock.replacedText.charAt(i)); - - if (textBlock[R].siblingCreated) textBlock[R].siblingCreated(cursor.options, L); - if (textBlock[L].siblingCreated) textBlock[L].siblingCreated(cursor.options, R); - textBlock.bubble(function (node) { node.reflow(); }); - }; - - _.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 this.ctrlSeq + '{' + contents.replace(/\\/g, '\\backslash ').replace(/[{}]/g, '\\$&') + '}'; - }; - _.html = function() { - return ( - '' - + this.textContents() - + '' - ); - }; - _.mathspeakTemplate = ['Start'+_.ariaLabel, 'End'+_.ariaLabel]; - _.mathspeak = function(opts) { - if (opts && opts.ignoreShorthand) { - return this.mathspeakTemplate[0]+', '+this.textContents() +', '+this.mathspeakTemplate[1] - } else { - return this.textContents(); - } - }; - _.isTextBlock = function() { - return true; - }; - - // 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); - aria.queueDirEndOf(-dir).queue(cursor.parent, true); - }; - _.moveOutOf = function(dir, cursor) { - cursor.insDirOf(dir, this); - aria.queueDirOf(dir).queue(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); // micro-optimization, not for correctness - } - this.bubble(function (node) { node.reflow(); }); - // TODO needs tests - aria.alert(ch); - }; - _.writeLatex = function(cursor, latex) { - if (!cursor[L]) TextPiece(latex).createLeftOf(cursor); - else cursor[L].appendText(latex); - this.bubble(function (node) { node.reflow(); }); - }; - - _.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); - }; - - _.mathspeak = - _.latex = function() { return this.text; }; - - _.deleteTowards = function(dir, cursor) { - if (this.text.length > 1) { - var deletedChar; - if (dir === R) { - this.dom.deleteData(0, 1); - deletedChar = this.text[0]; - 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); - deletedChar = this.text[this.text.length - 1]; - this.text = this.text.slice(0, -1); - } - aria.queue(deletedChar); - } - else { - this.remove(); - this.jQ.remove(); - cursor[dir] = this[dir]; - aria.queue(this.text); - } - }; - - _.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, ariaLabel, tagName, attrs) { - return P(TextBlock, { - ctrlSeq: latex, - ariaLabel: ariaLabel, - mathspeakTemplate: ['Start'+ariaLabel, 'End'+ariaLabel], - html: function() { - var cmdId = 'mathquill-command-id=' + this.id; - return '<'+tagName+' '+attrs+' '+cmdId+'>'+this.textContents()+''; - } - }); -} - -LatexCmds.em = LatexCmds.italic = LatexCmds.italics = -LatexCmds.emph = LatexCmds.textit = LatexCmds.textsl = - makeTextBlock('\\textit', 'Italic', 'i', 'class="mq-text-mode"'); -LatexCmds.strong = LatexCmds.bold = LatexCmds.textbf = - makeTextBlock('\\textbf', 'Bold', 'b', 'class="mq-text-mode"'); -LatexCmds.sf = LatexCmds.textsf = - makeTextBlock('\\textsf', 'Sans serif font', 'span', 'class="mq-sans-serif mq-text-mode"'); -LatexCmds.tt = LatexCmds.texttt = - makeTextBlock('\\texttt', 'Mono space font', 'span', 'class="mq-monospace mq-text-mode"'); -LatexCmds.textsc = - makeTextBlock('\\textsc', 'Variable font', 'span', 'style="font-variant:small-caps" class="mq-text-mode"'); -LatexCmds.uppercase = - makeTextBlock('\\uppercase', 'Uppercase', 'span', 'style="text-transform:uppercase" class="mq-text-mode"'); -LatexCmds.lowercase = - makeTextBlock('\\lowercase', '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 = '&0'; - _.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(); - }; - }); -}; diff --git a/src/commands/text.ts b/src/commands/text.ts new file mode 100644 index 000000000..2c348a1e9 --- /dev/null +++ b/src/commands/text.ts @@ -0,0 +1,470 @@ +/************************************************* + * 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. + */ +class TextBlock extends MQNode { + ctrlSeq = '\\text'; + ariaLabel = 'Text'; + replacedText?:string; + anticursorPosition?:number; + + replaces (replacedText:Fragment | string) { + if (replacedText instanceof Fragment) + this.replacedText = replacedText.remove().jQ.text(); + else if (typeof replacedText === 'string') + this.replacedText = replacedText; + }; + + jQadd (jQ:$) { + super.jQadd(jQ); + const endsL = this.ends[L]; + if (endsL) { + const child = this.jQ[0].firstChild; + if (child) { + endsL.jQadd(child); + } + } + return this.jQ; + }; + + createLeftOf (cursor:Cursor) { + var textBlock = this; + super.createLeftOf(cursor); + + cursor.insAtRightEnd(textBlock); + + if (textBlock.replacedText) + for (var i = 0; i < textBlock.replacedText.length; i += 1) + textBlock.write(cursor, textBlock.replacedText.charAt(i)); + + const textBlockR = textBlock[R]; + if (textBlockR) textBlockR.siblingCreated(cursor.options, L); + const textBlockL = textBlock[L]; + if (textBlockL) textBlockL.siblingCreated(cursor.options, R); + textBlock.bubble(function (node) { node.reflow(); return undefined}); + }; + + parser () { + 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 new Fragment(); + + new TextPiece(text).adopt(textBlock, 0, 0); + return textBlock; + }) as ParserAny + ; + }; + + textContents () { + return this.foldChildren('', function(text, child) { + return text + (child as TextPiece).textStr; + }); + }; + text () { return '"' + this.textContents() + '"'; }; + latex () { + var contents = this.textContents(); + if (contents.length === 0) return ''; + return this.ctrlSeq + '{' + contents.replace(/\\/g, '\\backslash ').replace(/[{}]/g, '\\$&') + '}'; + }; + html () { + return ( + '' + + this.textContents() + + '' + ); + }; + + mathspeakTemplate = ['StartText', 'EndText']; + mathspeak (opts?:MathspeakOptions) { + if (opts && opts.ignoreShorthand) { + return this.mathspeakTemplate[0]+', '+this.textContents() +', '+this.mathspeakTemplate[1] + } else { + return this.textContents(); + } + }; + isTextBlock () { + return true; + }; + + // 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 (dir:Direction, cursor:Cursor) { + cursor.insAtDirEnd(-dir as Direction, this); + cursor.controller.aria.queueDirEndOf(-dir as Direction).queue(cursor.parent, true); + }; + moveOutOf (dir:Direction, cursor:Cursor) { + cursor.insDirOf(dir, this); + cursor.controller.aria.queueDirOf(dir).queue(this); + }; + unselectInto (dir:Direction, cursor:Cursor) { + this.moveTowards(dir, cursor); + } + + // TODO: make these methods part of a shared mixin or something. + selectTowards (dir:Direction, cursor:Cursor) { + MathCommand.prototype.selectTowards.call(this, dir, cursor); + } + deleteTowards (dir:Direction, cursor:Cursor) { + MathCommand.prototype.deleteTowards.call(this, dir, cursor); + } + + selectOutOf (dir:Direction, cursor:Cursor) { + cursor.insDirOf(dir, this); + }; + deleteOutOf (_dir:Direction, cursor:Cursor) { + // backspace and delete at ends of block don't unwrap + if (this.isEmpty()) cursor.insRightOf(this); + }; + write (cursor:Cursor, ch:string) { + cursor.show().deleteSelection(); + + if (ch !== '$') { + let cursorL = cursor[L]; + if (!cursorL) new TextPiece(ch).createLeftOf(cursor); + else if (cursorL instanceof TextPiece) cursorL.appendText(ch); + } + else if (this.isEmpty()) { + cursor.insRightOf(this); + new VanillaSymbol('\\$','$').createLeftOf(cursor); + } + else if (!cursor[R]) cursor.insRightOf(this); + else if (!cursor[L]) cursor.insLeftOf(this); + else { // split apart + var leftBlock = new TextBlock(); + var leftPc = this.ends[L]; + if (leftPc) { + leftPc.disown().jQ.detach(); + leftPc.adopt(leftBlock, 0, 0); + } + + cursor.insLeftOf(this); + super.createLeftOf.call(leftBlock, cursor); // micro-optimization, not for correctness + } + this.bubble(function (node) { node.reflow(); return undefined; }); + // TODO needs tests + cursor.controller.aria.alert(ch); + }; + writeLatex (cursor:Cursor, latex:string) { + const cursorL = cursor[L]; + if (!cursorL) new TextPiece(latex).createLeftOf(cursor); + else if (cursorL instanceof TextPiece) cursorL.appendText(latex); + this.bubble(function (node) { node.reflow(); return undefined }); + }; + + seek (pageX:number, cursor:Cursor) { + cursor.hide(); + var textPc = TextBlockFuseChildren(this); + if (!textPc) return; + + // 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.textStr.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 as number; + // displ * prevDispl > 0 iff displacement direction === previous direction + while (cursor[dir] && displ * prevDispl > 0) { + (cursor[dir] as MQNode).moveTowards(dir, cursor); + prevDispl = displ; + displ = pageX - cursor.offset().left; + } + if (dir*displ < -dir*prevDispl) (cursor[-dir as Direction] as MQNode).moveTowards(-dir as Direction, cursor); + + if (!cursor.anticursor) { + // about to start mouse-selecting, the anticursor is gonna get put here + const cursorL = cursor[L]; + this.anticursorPosition = cursorL && (cursorL as TextPiece).textStr.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 + const cursorL = cursor[L]; + var cursorPosition = cursorL && (cursorL as TextPiece).textStr.length; + if (this.anticursorPosition === cursorPosition) { + cursor.anticursor = Anticursor.fromCursor(cursor); + } + else { + if (this.anticursorPosition! < cursorPosition!) { + var newTextPc = (cursorL as any as TextPiece).splitRight(this.anticursorPosition!); + cursor[L] = newTextPc; + } + else { + const cursorR = cursor[R] as any as TextPiece; + var newTextPc = cursorR.splitRight(this.anticursorPosition! - cursorPosition!); + } + cursor.anticursor = new Anticursor(this, newTextPc[L], newTextPc); + } + } + }; + + blur (cursor:Cursor) { + MathBlock.prototype.blur.call(this, cursor); + 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 TextBlockFuseChildren(this); + }; + + focus () { + MathBlock.prototype.focus.call(this); + } +}; + +function TextBlockFuseChildren(self:TextBlock) { + self.jQ[0].normalize(); + + var textPcDom = self.jQ[0].firstChild as Text; + 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 = new TextPiece(textPcDom.data); + textPc.jQadd(textPcDom); + + self.children().disown(); + textPc.adopt(self, 0, 0); + return textPc; +} + +/** + * Piece of plain text, with a TextBlock as a parent and no children. + * Wraps a single DOMTextNode. + * For convenience, has a .textStr property that's just a JavaScript string + * mirroring the text contents of the DOMTextNode. + * Text contents must always be nonempty. + */ +class TextPiece extends MQNode { + textStr: string; + dom:Text; + + constructor (text:string) { + super(); + this.textStr = text; + }; + jQadd (dom:Text) { + this.dom = dom; + this.jQ = $(dom); + return this.jQ; + }; + jQize () { + return this.jQadd(document.createTextNode(this.textStr)); + }; + appendText (text:string) { + this.textStr += text; + this.dom.appendData(text); + }; + prependText (text:string) { + this.textStr = text + this.textStr; + this.dom.insertData(0, text); + }; + insTextAtDirEnd (text:string, dir:Direction) { + prayDirection(dir); + if (dir === R) this.appendText(text); + else this.prependText(text); + }; + splitRight (i:number) { + var newPc = new TextPiece(this.textStr.slice(i)).adopt(this.parent, this, this[R]); + newPc.jQadd(this.dom.splitText(i)); + this.textStr = this.textStr.slice(0, i); + return newPc; + }; + + endChar(dir:Direction, text:string) { + return text.charAt(dir === L ? 0 : -1 + text.length); + } + + moveTowards (dir:Direction, cursor:Cursor) { + prayDirection(dir); + + var ch = this.endChar(-dir as Direction, this.textStr) + + var from = this[-dir as Direction]; + if (from instanceof TextPiece) from.insTextAtDirEnd(ch, dir); + else new TextPiece(ch).createDir(-dir as Direction, cursor); + return this.deleteTowards(dir, cursor); + }; + + mathspeak () { return this.textStr; }; + latex () { return this.textStr; }; + + deleteTowards (dir:Direction, cursor:Cursor) { + if (this.textStr.length > 1) { + var deletedChar; + if (dir === R) { + this.dom.deleteData(0, 1); + deletedChar = this.textStr[0]; + this.textStr = this.textStr.slice(1); + } + else { + // note that the order of these 2 lines is annoyingly important + // (the second line mutates this.textStr.length) + this.dom.deleteData(-1 + this.textStr.length, 1); + deletedChar = this.textStr[this.textStr.length - 1]; + this.textStr = this.textStr.slice(0, -1); + } + cursor.controller.aria.queue(deletedChar); + } + else { + this.remove(); + this.jQ.remove(); + cursor[dir] = this[dir]; + cursor.controller.aria.queue(this.textStr); + } + }; + + selectTowards (dir:Direction, cursor:Cursor) { + prayDirection(dir); + var anticursor = cursor.anticursor; + if (!anticursor) return; + + var ch = this.endChar(-dir as Direction, this.textStr) + + if (anticursor[dir] === this) { + var newPc = new TextPiece(ch).createDir(dir, cursor); + anticursor[dir] = newPc; + cursor.insDirOf(dir, newPc); + } + else { + var from = this[-dir as Direction]; + if (from instanceof TextPiece) from.insTextAtDirEnd(ch, dir); + else { + var newPc = new TextPiece(ch).createDir(-dir as Direction, cursor); + var selection = cursor.selection; + if (selection) { + newPc.jQ.insDirOf(-dir as Direction, selection.jQ); + } + } + + if (this.textStr.length === 1 && anticursor[-dir as Direction] === this) { + anticursor[-dir as Direction] = this[-dir as Direction]; // `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:string, ariaLabel:string, tagName:string, attrs:string) { + return class extends TextBlock { + ctrlSeq = latex; + mathspeakTemplate = ['Start'+ariaLabel, 'End'+ariaLabel]; + ariaLabel = ariaLabel; + + html () { + var cmdId = 'mathquill-command-id=' + this.id; + return '<'+tagName+' '+attrs+' '+cmdId+'>'+this.textContents()+''; + } + }; +} + +LatexCmds.em = LatexCmds.italic = LatexCmds.italics = +LatexCmds.emph = LatexCmds.textit = LatexCmds.textsl = + makeTextBlock('\\textit', 'Italic', 'i', 'class="mq-text-mode"'); +LatexCmds.strong = LatexCmds.bold = LatexCmds.textbf = + makeTextBlock('\\textbf', 'Bold', 'b', 'class="mq-text-mode"'); +LatexCmds.sf = LatexCmds.textsf = + makeTextBlock('\\textsf', 'Sans serif font', 'span', 'class="mq-sans-serif mq-text-mode"'); +LatexCmds.tt = LatexCmds.texttt = + makeTextBlock('\\texttt', 'Mono space font', 'span', 'class="mq-monospace mq-text-mode"'); +LatexCmds.textsc = + makeTextBlock('\\textsc', 'Variable font', 'span', 'style="font-variant:small-caps" class="mq-text-mode"'); +LatexCmds.uppercase = + makeTextBlock('\\uppercase', 'Uppercase', 'span', 'style="text-transform:uppercase" class="mq-text-mode"'); +LatexCmds.lowercase = + makeTextBlock('\\lowercase', 'Lowercase', 'span', 'style="text-transform:lowercase" class="mq-text-mode"'); + + +class RootMathCommand extends MathCommand { + cursor:Cursor; + constructor (cursor:Cursor) { + super('$'); + this.cursor = cursor; + }; + htmlTemplate = '&0'; + createBlocks () { + super.createBlocks() + const endsL = this.ends[L] as RootMathCommand; // TODO - how do we know this is a RootMathCommand? + endsL.cursor = this.cursor; + endsL.write = function(cursor:Cursor, ch:string) { + if (ch !== '$') + MathBlock.prototype.write.call(this, cursor, ch); + else if (this.isEmpty()) { + cursor.insRightOf(this.parent); + this.parent.deleteTowards(undefined!, cursor); + new 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 () { + return '$' + (this.ends[L] as MQNode).latex() + '$'; + }; +}; + +class RootTextBlock extends RootMathBlock { + keystroke (key:string, e:KeyboardEvent, ctrlr:Controller) { + if (key === 'Spacebar' || key === 'Shift-Spacebar') return; + return super.keystroke(key, e, ctrlr); + }; + write (cursor:Cursor, ch:string) { + cursor.show().deleteSelection(); + if (ch === '$') + new RootMathCommand(cursor).createLeftOf(cursor); + else { + var html; + if (ch === '<') html = '<'; + else if (ch === '>') html = '>'; + new VanillaSymbol(ch, html).createLeftOf(cursor); + } + }; +}; +API.TextField = function(APIClasses:APIClasses) { + return class extends APIClasses.EditableField { + static RootBlock = RootTextBlock; + __mathquillify () { + return super.__mathquillify('mq-editable-field mq-text-mode'); + }; + latex (latex:string) { + if (arguments.length > 0) { + this.__controller.renderLatexText(latex); + if (this.__controller.blurred) this.__controller.cursor.hide().parent.blur(); + return this; + } + return this.__controller.exportLatex(); + }; + }; +}; diff --git a/src/controller.js b/src/controller.ts similarity index 54% rename from src/controller.js rename to src/controller.ts index 3d2b999f3..5d3815650 100644 --- a/src/controller.js +++ b/src/controller.ts @@ -1,47 +1,64 @@ /********************************************* - * Controller for a MathQuill instance, - * on which services are registered with - * - * Controller.open(function(_) { ... }); - * + * Controller for a MathQuill instance ********************************************/ +class ControllerBase { + id:number; + data:ControllerData; + root:ControllerRoot; + container:$; + options:CursorOptions; + aria:Aria; + ariaLabel:string; + ariaPostLabel:string; + cursor:Cursor; + editable:boolean | undefined; + _ariaAlertTimeout:number; + KIND_OF_MQ:KIND_OF_MQ; + textarea:$ | undefined; + textareaSpan:$ | undefined; + mathspeakSpan:$ | undefined; -var Controller = P(function(_) { - _.init = function(root, container, options) { + constructor (root:ControllerRoot, container:$, options:CursorOptions) { this.id = root.id; this.data = {}; this.root = root; this.container = container; this.options = options; - + + this.aria = new Aria(this.getControllerSelf()); this.ariaLabel = 'Math Input'; this.ariaPostLabel = ''; - root.controller = this; + root.controller = this.getControllerSelf(); - this.cursor = root.cursor = Cursor(root, options, this); + this.cursor = root.cursor = new Cursor(root, options, this.getControllerSelf()); // TODO: stop depending on root.cursor, and rm it }; - _.handle = function(name, dir) { + getControllerSelf() { + // dance we have to do to tell this thing it's a full controller + return this as any as Controller; + } + + handle (name:HandlerName, dir?:Direction) { var handlers = this.options.handlers; if (handlers && handlers.fns[name]) { - var mq = handlers.APIClasses[this.KIND_OF_MQ](this); + var mq = new 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); + static notifyees:((cursor:Cursor, e:ControllerEvent) => void)[] = []; + static onNotify (f:(cursor:Cursor, e:ControllerEvent) => void) { ControllerBase.notifyees.push(f); }; + notify (e:ControllerEvent) { + for (var i = 0; i < ControllerBase.notifyees.length; i += 1) { + ControllerBase.notifyees[i](this.cursor, e); } return this; }; - _.setAriaLabel = function(ariaLabel) { + setAriaLabel (ariaLabel:string) { var oldAriaLabel = this.getAriaLabel(); if (ariaLabel && typeof ariaLabel === 'string' && ariaLabel !== '') { this.ariaLabel = ariaLabel; @@ -59,7 +76,7 @@ var Controller = P(function(_) { } return this; }; - _.getAriaLabel = function () { + getAriaLabel () { if (this.ariaLabel !== 'Math Input') { return this.ariaLabel; } else if (this.editable) { @@ -68,22 +85,22 @@ var Controller = P(function(_) { return ''; } }; - _.setAriaPostLabel = function(ariaPostLabel, timeout) { + setAriaPostLabel (ariaPostLabel:string, timeout:number) { if(ariaPostLabel && typeof ariaPostLabel === 'string' && ariaPostLabel !== '') { if ( ariaPostLabel !== this.ariaPostLabel && typeof timeout === 'number' ) { if (this._ariaAlertTimeout) clearTimeout(this._ariaAlertTimeout); - this._ariaAlertTimeout = setTimeout(function() { + this._ariaAlertTimeout = setTimeout(() => { if (this.containerHasFocus()) { // Voice the new label, but do not update content mathspeak to prevent double-speech. - aria.alert(this.root.mathspeak().trim() + ' ' + ariaPostLabel.trim()); + this.aria.alert(this.root.mathspeak().trim() + ' ' + ariaPostLabel.trim()); } else { // This mathquill does not have focus, so update its mathspeak. this.updateMathspeak(); } - }.bind(this), timeout); + }, timeout); } this.ariaPostLabel = ariaPostLabel; } else { @@ -92,10 +109,10 @@ var Controller = P(function(_) { } return this; }; - _.getAriaPostLabel = function () { + getAriaPostLabel () { return this.ariaPostLabel || ''; }; - _.containerHasFocus = function () { + containerHasFocus () { return ( document.activeElement && this.container && @@ -103,4 +120,26 @@ var Controller = P(function(_) { this.container[0].contains(document.activeElement) ); }; -}); + + getTextareaOrThrow () { + var textarea = this.textarea; + if (!textarea) throw new Error('expected a textarea'); + return textarea; + } + + getTextareaSpanOrThrow () { + var textareaSpan = this.textareaSpan; + if (!textareaSpan) throw new Error('expected a textareaSpan'); + return textareaSpan; + } + + // based on http://www.gh-mathspeak.com/examples/quick-tutorial/ + // and http://www.gh-mathspeak.com/examples/grammar-rules/ + exportMathSpeak () { return this.root.mathspeak(); }; + + // overridden + updateMathspeak () {}; + scrollHoriz () {}; + selectionChanged () {}; + setOverflowClasses () {}; +}; \ No newline at end of file diff --git a/src/css/math.less b/src/css/math.less index 107190a62..36f8710ea 100644 --- a/src/css/math.less +++ b/src/css/math.less @@ -94,7 +94,7 @@ &.mq-root-block { background: transparent; } - &.mq-empty-parens, &.mq-empty-square-brackets { + &.mq-quiet-delimiter { background: transparent } } @@ -105,7 +105,7 @@ .mq-text-mode { display: inline-block; - white-space: pre; + white-space: pre; } .mq-text-mode.mq-hasCursor { @@ -430,6 +430,7 @@ margin-top: 1px; padding-top: 0.2em; text-align: center; + position: relative; &:after { position: absolute; @@ -456,34 +457,5 @@ filter: FlipH; -ms-filter: "FlipH"; } - &.mq-arrow-both { - vertical-align: text-bottom; - - &.mq-empty { - min-height: 1.23em; - - &:after { - top: -0.34em; - } - } - &:before{ - -moz-transform: scaleX(-1); - -o-transform: scaleX(-1); - -webkit-transform: scaleX(-1); - transform: scaleX(-1); - filter: FlipH; - -ms-filter: "FlipH"; - } - &:after { - display: block; - position: relative; - top: -2.3em; - font-size: 0.5em; - line-height: 0em; - content: '\27A4'; - visibility: visible; //must override .mq-editable-field.mq-empty:after - text-align: right; - } - } } } diff --git a/src/cursor.js b/src/cursor.ts similarity index 60% rename from src/cursor.js rename to src/cursor.ts index 3df5ac0b6..26e7349ac 100644 --- a/src/cursor.js +++ b/src/cursor.ts @@ -10,29 +10,53 @@ 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, controller) { +class Anticursor extends Point { + ancestors:Record = {}; + constructor (parent: MQNode, leftward?:NodeRef, rightward?:NodeRef) { + super(parent, leftward, rightward) + } + + static fromCursor (cursor:Cursor) { + return new Anticursor(cursor.parent, cursor[L], cursor[R]); + } +} + +class Cursor extends Point { + controller:Controller + parent:MQNode; + options:CursorOptions; + /** Slightly more than just a "cache", this remembers the cursor's position in each block node, so that we can return to the right + * point in that node when moving up and down among blocks. + */ + upDownCache:Record = {}; + blink: () => void; + _jQ:$; + jQ:$; + selection:MQSelection | undefined; + intervalId:number; + anticursor:Anticursor | undefined; + + constructor (initParent:MQNode, options:CursorOptions, controller:Controller) { + super(initParent); this.controller = controller; - this.parent = initParent; this.options = options; var jQ = this.jQ = this._jQ = $(''); //closured for setInterval this.blink = function(){ jQ.toggleClass('mq-blink'); }; - - this.upDownCache = {}; }; - _.show = function() { + show () { this.jQ = this._jQ.removeClass('mq-blink'); - if ('intervalId' in this) //already was shown, just restart interval + if (this.intervalId) //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); + var selection = this.selection; + if ( selection && (selection.ends[L] as MQNode)[L] === this[L]) + this.jQ.insertBefore(selection.jQ); else - this.jQ.insertBefore(this[R].jQ.first()); + this.jQ.insertBefore((this[R] as MQNode).jQ.first()); } else this.jQ.appendTo(this.parent.jQ); @@ -41,44 +65,46 @@ var Cursor = P(Point, function(_) { this.intervalId = setInterval(this.blink, 500); return this; }; - _.hide = function() { - if ('intervalId' in this) + hide () { + if (this.intervalId) clearInterval(this.intervalId); - delete this.intervalId; + this.intervalId = 0; this.jQ.detach(); this.jQ = $(); return this; }; - _.withDirInsertAt = function(dir, parent, withDir, oppDir) { + withDirInsertAt (dir:Direction, parent:MQNode, withDir:NodeRef, oppDir:NodeRef) { var oldParent = this.parent; this.parent = parent; - this[dir] = withDir; - this[-dir] = oppDir; + this[dir as Direction] = withDir; + this[-dir as Direction] = 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) { + /** Place the cursor before or after `el`, according the side specified by `dir`. */ + insDirOf (dir:Direction, el:MQNode) { 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); }; + insLeftOf (el:MQNode) { return this.insDirOf(L, el); }; + insRightOf (el:MQNode) { return this.insDirOf(R, el); }; - _.insAtDirEnd = function(dir, el) { + /** Place the cursor inside `el` at either the left or right end, according the side specified by `dir`. */ + insAtDirEnd (dir:Direction, el:MQNode) { 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); }; + insAtLeftEnd (el:MQNode) { return this.insAtDirEnd(L, el); }; + insAtRightEnd (el:MQNode) { return this.insAtDirEnd(R, el); }; /** * jump up or down from one block Node to another: @@ -88,20 +114,25 @@ var Cursor = P(Point, function(_) { * + if not seek a position in the node that is horizontally closest to * the cursor's current position */ - _.jumpUpDown = function(from, to) { + jumpUpDown (from:MQNode, to:MQNode) { 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); + var cachedR = cached[R]; + if (cachedR) { + self.insLeftOf(cachedR) + } else { + self.insAtRightEnd(cached.parent); + } } else { var pageX = self.offset().left; to.seek(pageX, self); } - aria.queue(to, true); + self.controller.aria.queue(to, true); }; - _.offset = function() { + offset () { //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 @@ -113,7 +144,7 @@ var Cursor = P(Point, function(_) { self.jQ.addClass('mq-cursor'); return offset; } - _.unwrapGramp = function() { + unwrapGramp () { var gramp = this.parent.parent; var greatgramp = gramp.parent; var rightward = gramp[R]; @@ -121,27 +152,31 @@ var Cursor = P(Point, function(_) { var leftward = gramp[L]; gramp.disown().eachChild(function(uncle) { - if (uncle.isEmpty()) return; + if (uncle.isEmpty()) return true; uncle.children() .adopt(greatgramp, leftward, rightward) .each(function(cousin) { cousin.jQ.insertBefore(gramp.jQ.first()); + return true; }) ; leftward = uncle.ends[R]; + return true; }); if (!this[R]) { //then find something to be rightward to insLeftOf - if (this[L]) - this[R] = this[L][R]; + var thisL = this[L]; + if (thisL) + this[R] = thisL[R]; else { while (!this[R]) { - this.parent = this.parent[R]; - if (this.parent) - this[R] = this.parent.ends[L]; - else { + var newParent = this.parent[R]; + if (newParent) { + this.parent = newParent; + this[R] = newParent.ends[L]; + } else { this[R] = gramp[R]; this.parent = greatgramp; break; @@ -149,41 +184,47 @@ var Cursor = P(Point, function(_) { } } } - if (this[R]) - this.insLeftOf(this[R]); + + var thisR = this[R]; + if (thisR) + this.insLeftOf(thisR); 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); + var grampL = gramp[L]; + var grampR = gramp[R]; + if (grampL) grampL.siblingDeleted(cursor.options, R); + if (grampR) grampR.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) { + startSelection () { + var anticursor = this.anticursor = Anticursor.fromCursor(this); + var ancestors = anticursor.ancestors; + + for (var ancestor:MQNode | Anticursor= anticursor; ancestor.parent; ancestor = ancestor.parent) { ancestors[ancestor.parent.id] = ancestor; } }; - _.endSelection = function() { + endSelection () { delete this.anticursor; }; - _.select = function() { - var anticursor = this.anticursor; + select () { + var _lca; + 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) { + for (var ancestor:MQNode | Point | undefined = this; ancestor.parent; ancestor = ancestor.parent) { if (ancestor.parent.id in anticursor.ancestors) { - var lca = ancestor.parent; + _lca = ancestor.parent; break; } } - pray('cursor and anticursor in the same tree', lca); + pray('cursor and anticursor in the same tree', _lca); + var lca = _lca as MQNode; + // 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 @@ -192,13 +233,13 @@ var Cursor = P(Point, function(_) { // The other end of the selection fragment, the ancestor of the anticursor // whose parent is the LCA. - var antiAncestor = anticursor.ancestors[lca.id]; + var antiAncestor = anticursor.ancestors[lca.id] as MQNode; // 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; + var leftEnd, rightEnd, dir:Direction = R; // This is an extremely subtle algorithm. // As a special case, `ancestor` could be a Point and `antiAncestor` a Node @@ -211,7 +252,7 @@ var Cursor = P(Point, function(_) { // `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]) { + for (var rightward:NodeRef | Point | undefined = ancestor; rightward; rightward = rightward[R]) { if (rightward[R] === antiAncestor[R]) { dir = L; leftEnd = ancestor; @@ -229,19 +270,21 @@ var Cursor = P(Point, function(_) { 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.hide().selection = lca.selectChildren(leftEnd as MQNode, rightEnd as MQNode); + + var insEl = this.selection!.ends[dir] as MQNode; + this.insDirOf(dir, insEl); this.selectionChanged(); return true; }; - _.resetToEnd = function (controller) { + resetToEnd (controller:ControllerBase) { this.clearSelection(); var root = controller.root; this[R] = 0; this[L] = root.ends[R]; this.parent = root; }; - _.clearSelection = function() { + clearSelection () { if (this.selection) { this.selection.clear(); delete this.selection; @@ -249,59 +292,65 @@ var Cursor = P(Point, function(_) { } return this; }; - _.deleteSelection = function() { - if (!this.selection) return; + deleteSelection () { + var selection = this.selection; + if (!selection) return; - this[L] = this.selection.ends[L][L]; - this[R] = this.selection.ends[R][R]; - this.selection.remove(); + this[L] = (selection.ends[L] as MQNode)[L]; + this[R] = (selection.ends[R] as MQNode)[R]; + selection.remove(); this.selectionChanged(); delete this.selection; }; - _.replaceSelection = function() { + replaceSelection () { var seln = this.selection; if (seln) { - this[L] = seln.ends[L][L]; - this[R] = seln.ends[R][R]; + this[L] = (seln.ends[L] as MQNode)[L]; + this[R] = (seln.ends[R] as MQNode)[R]; delete this.selection; } return seln; }; - _.depth = function() { - var node = this; + depth () { + var node:MQNode | Point = this; var depth = 0; while (node = node.parent) { depth += (node instanceof MathBlock) ? 1 : 0; } return depth; }; - _.isTooDeep = function(offset) { + isTooDeep (offset?:number) { if (this.options.maxDepth !== undefined) { return this.depth() + (offset || 0) > this.options.maxDepth; + } else { + return false; } }; -}); -var Selection = P(Fragment, function(_, super_) { - _.init = function() { - super_.init.apply(this, arguments); + + // can be overridden + selectionChanged () {} +} +class MQSelection extends Fragment { + constructor (withDir:MQNode, oppDir:MQNode, dir?:Direction) { + super(withDir, oppDir, dir); + this.jQ = this.jQ.wrapAll('').parent(); //can't do wrapAll(this.jQ = $(...)) because wrapAll will clone it }; - _.adopt = function() { + adopt (parent:MQNode, leftward:NodeRef, rightward:NodeRef) { this.jQ.replaceWith(this.jQ = this.jQ.children()); - return super_.adopt.apply(this, arguments); + return super.adopt(parent, leftward, rightward); }; - _.clear = function() { + clear () { // 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, separatorToken) { - var separator = separatorToken || ''; + join (methodName:JoinMethod, separator:string = ''):string { return this.fold('', function(fold, child) { return fold + separator + child[methodName](); }); }; -}); +}; diff --git a/src/intro.js b/src/intro.js index 1ff07bbb6..593aa7117 100644 --- a/src/intro.js +++ b/src/intro.js @@ -9,38 +9,3 @@ */ (function() { - -var jQuery = window.jQuery, - undefined, - min = Math.min, - max = Math.max; - -if (!jQuery) throw 'MathQuill requires jQuery 1.5.2+ to be loaded first'; - -function noop() {} - -/** - * sugar to make defining lots of commands easier. - * TODO: rethink this. - */ -var __slice = [].slice; -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); -} diff --git a/src/publicapi.js b/src/publicapi.ts similarity index 56% rename from src/publicapi.js rename to src/publicapi.ts index c3a2dab1a..9349eee04 100644 --- a/src/publicapi.js +++ b/src/publicapi.ts @@ -2,7 +2,61 @@ * The publicly exposed MathQuill API. ********************************************************/ -var API = {}, Options = P(), optionProcessors = {}, Progenote = P(), EMBEDS = {}; +var API:API = { + +}; + +var EMBEDS:Record EmbedOptions> = {}; + +class OptionProcessors { + maxDepth: (n:number) => CursorOptions['maxDepth']; + leftRightIntoCmdGoes: (s:'up'|'down') => CursorOptions['leftRightIntoCmdGoes']; + autoCommands: (list:string) => CursorOptions['autoOperatorNames']; + autoOperatorNames: (list:string) => CursorOptions['autoOperatorNames']; + autoParenthesizedFunctions: (list:string) => CursorOptions['autoOperatorNames']; + quietEmptyDelimiters: (list:string) => CursorOptions['quietEmptyDelimiters']; +} + +const optionProcessors = new OptionProcessors(); +type AutoDict = { + _maxLength?: number; + [id:string]:any; +} + +class Options { + ignoreNextMousedown: (_el:MouseEvent) => boolean; + substituteTextarea: () => HTMLElement; + substituteKeyboardEvents:typeof saneKeyboardEvents; + + restrictMismatchedBrackets?:boolean; + typingSlashCreatesNewFraction?:boolean; + charsThatBreakOutOfSupSub:string; + sumStartsWithNEquals?:boolean; + autoSubscriptNumerals?: boolean; + supSubsRequireOperand?: boolean; + spaceBehavesLikeTab?: boolean; + typingAsteriskWritesTimesSymbol?: boolean; + typingSlashWritesDivisionSymbol: boolean; + typingPercentWritesPercentOf?: boolean; + resetCursorOnBlur?: boolean | undefined; + leftRightIntoCmdGoes?: 'up' | 'down'; + enableDigitGrouping?: boolean; + mouseEvents?: boolean; + maxDepth?: number; + disableCopyPaste?: boolean; + statelessClipboard?: boolean; + onPaste?: () => void; + onCut?: () => void; + overrideTypedText?:(text:string) => void; + overrideKeystroke: (key:string, event:KeyboardEvent) => void; + autoOperatorNames: AutoDict; + autoCommands: AutoDict; + autoParenthesizedFunctions: AutoDict; + quietEmptyDelimiters: { [id:string]:any; }; + disableAutoSubstitutionInSubscripts?: boolean; + handlers: HandlerOptions +}; +class Progenote {} /** * Interface Versioning (#459, #495) to allow us to virtually guarantee @@ -12,7 +66,7 @@ var API = {}, Options = P(), optionProcessors = {}, Progenote = P(), EMBEDS = {} * The methods are shimmed in outro.js so that MQ.MathField.prototype etc can * be accessed. */ -function insistOnInterVer() { +var insistOnInterVer = function() { 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 ' + @@ -26,13 +80,13 @@ function insistOnInterVer() { ); } // globally exported API object -function MathQuill(el) { +function MathQuill(el:HTMLElement) { insistOnInterVer(); return MQ1(el); }; -MathQuill.prototype = Progenote.p; +MathQuill.prototype = Progenote.prototype; MathQuill.VERSION = "{VERSION}"; -MathQuill.interfaceVersion = function(v) { +MathQuill.interfaceVersion = function(v:number) { // shim for #459-era interface versioning (ended with #495) if (v !== 1) throw 'Only interface version 1 supported. You specified: ' + v; insistOnInterVer = function() { @@ -54,7 +108,7 @@ MathQuill.interfaceVersion = function(v) { MathQuill.getInterface = getInterface; var MIN = getInterface.MIN = 1, MAX = getInterface.MAX = 2; -function getInterface(v) { +function getInterface(v:number) { if (!(MIN <= v && v <= MAX)) throw 'Only interface versions between ' + MIN + ' and ' + MAX + ' supported. You specified: ' + v; @@ -67,50 +121,57 @@ function getInterface(v) { * assert(MQ(mathFieldSpan).id === MQ(mathFieldSpan).id); * */ - function MQ(el) { + var MQ:MQ = function (el:HTMLElement) { 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 blockNode = Node.getNodeOfElement($(el).children('.mq-root-block')[0]); + var blockNode = NodeBase.getNodeOfElement($(el).children('.mq-root-block')[0]) as MathBlock; // TODO - assumng it's a MathBlock var ctrlr = blockNode && blockNode.controller; - return ctrlr ? APIClasses[ctrlr.KIND_OF_MQ](ctrlr) : null; + return ctrlr ? new APIClasses[ctrlr.KIND_OF_MQ](ctrlr) : null; }; - var APIClasses = {}; MQ.L = L; MQ.R = R; MQ.saneKeyboardEvents = saneKeyboardEvents; - function config(currentOptions, newOptions) { + function config(currentOptions:CursorOptions, newOptions:CursorOptions) { 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); + var value = (newOptions as any)[name]; // TODO - think about typing this better + var processor = (optionProcessors as any)[name]; // TODO - validate option processors better + (currentOptions as any)[name] = (processor ? processor(value) : value); // TODO - think about typing better } } - MQ.config = function(opts) { config(Options.p, opts); return this; }; - MQ.registerEmbed = function(name, options) { + MQ.config = function(opts:CursorOptions) { config(Options.prototype, opts); return this; }; + MQ.registerEmbed = function(name:string, options:(data:EmbedOptionsData) => EmbedOptions) { 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) { + class AbstractMathQuill extends Progenote { + __controller:Controller; + __options:CursorOptions; + id:number; + data:ControllerData; + revert?: () => $; + + constructor (ctrlr:Controller) { + super(); this.__controller = ctrlr; this.__options = ctrlr.options; this.id = ctrlr.id; this.data = ctrlr.data; }; - _.__mathquillify = function(classNames) { + __mathquillify (classNames:string) { var ctrlr = this.__controller, root = ctrlr.root, el = ctrlr.container; ctrlr.createTextarea(); var contents = el.addClass(classNames).contents().detach(); root.jQ = $('').appendTo(el); - Node.linkElementByBlockId(root.jQ[0], root.id); + NodeBase.linkElementByBlockId(root.jQ[0], root.id); this.latex(contents.text()); this.revert = function() { @@ -119,53 +180,55 @@ function getInterface(v) { .append(contents); }; }; - _.config = function(opts) { config(this.__options, opts); return this; }; - _.el = function() { return this.__controller.container[0]; }; - _.text = function() { return this.__controller.exportText(); }; - _.mathspeak = function() { return this.__controller.exportMathSpeak(); }; - _.latex = function(latex) { + config (opts:CursorOptions) { config(this.__options, opts); return this; }; + el () { return this.__controller.container[0]; }; + text () { return this.__controller.exportText(); }; + mathspeak () { return this.__controller.exportMathSpeak(); }; + latex (latex:string) { if (arguments.length > 0) { this.__controller.renderLatexMath(latex); - if (this.__controller.blurred) this.__controller.cursor.hide().parent.blur(); + const cursor = this.__controller.cursor; + if (this.__controller.blurred) cursor.hide().parent.blur(cursor); return this; } return this.__controller.exportLatex(); }; - _.html = function() { + html () { return this.__controller.root.jQ.html() .replace(/ mathquill-(?:command|block)-id="?\d+"?/g, '') .replace(/.?<\/span>/i, '') .replace(/ mq-hasCursor|mq-hasCursor ?/, '') .replace(/ class=(""|(?= |>))/g, ''); }; - _.reflow = function() { + reflow () { this.__controller.root.postOrder(function (node) { node.reflow(); }); return this; }; - }); + }; MQ.prototype = AbstractMathQuill.prototype; - APIClasses.EditableField = P(AbstractMathQuill, function(_, super_) { - _.__mathquillify = function() { - super_.__mathquillify.apply(this, arguments); + class EditableField extends AbstractMathQuill { + __mathquillify (classNames:string) { + super.__mathquillify(classNames); this.__controller.editable = true; this.__controller.delegateMouseEvents(); this.__controller.editablesTextareaEvents(); return this; }; - _.focus = function() { - this.__controller.textarea[0].focus(); + focus () { + this.__controller.getTextareaOrThrow()[0].focus(); this.__controller.scrollHoriz(); return this; }; - _.blur = function() { this.__controller.textarea.blur(); return this; }; - _.write = function(latex) { + blur () { this.__controller.getTextareaOrThrow().blur(); return this; }; + write (latex:string) { this.__controller.writeLatex(latex); this.__controller.scrollHoriz(); - if (this.__controller.blurred) this.__controller.cursor.hide().parent.blur(); + const cursor = this.__controller.cursor; + if (this.__controller.blurred) cursor.hide().parent.blur(cursor); return this; }; - _.empty = function() { + empty () { var root = this.__controller.root, cursor = this.__controller.cursor; root.ends[L] = root.ends[R] = 0; @@ -174,77 +237,82 @@ function getInterface(v) { cursor.insAtRightEnd(root); return this; }; - _.cmd = function(cmd) { - var ctrlr = this.__controller.notify(), cursor = ctrlr.cursor; + cmd (cmd:string) { + var ctrlr = this.__controller.notify(undefined), cursor = ctrlr.cursor; if (/^\\[a-z]+$/i.test(cmd) && !cursor.isTooDeep()) { cmd = cmd.slice(1); - var klass = LatexCmds[cmd]; + var klass = (LatexCmds as LatexCmdsAny)[cmd]; + var node; if (klass) { - cmd = klass(cmd); - if (cursor.selection) cmd.replaces(cursor.replaceSelection()); - cmd.createLeftOf(cursor.show()); + if (klass.constructor) { + node = new klass(cmd); + } else { + node = klass(cmd); + } + if (cursor.selection) node.replaces(cursor.replaceSelection()); + node.createLeftOf(cursor.show()); } else /* TODO: API needs better error reporting */; } else cursor.parent.write(cursor, cmd); ctrlr.scrollHoriz(); - if (ctrlr.blurred) cursor.hide().parent.blur(); + if (ctrlr.blurred) cursor.hide().parent.blur(cursor); return this; }; - _.select = function() { + select () { var ctrlr = this.__controller; ctrlr.notify('move').cursor.insAtRightEnd(ctrlr.root); while (ctrlr.cursor[L]) ctrlr.selectLeft(); return this; }; - _.clearSelection = function() { + clearSelection () { this.__controller.cursor.clearSelection(); return this; }; - _.moveToDirEnd = function(dir) { + moveToDirEnd (dir:Direction) { this.__controller.notify('move').cursor.insAtDirEnd(dir, this.__controller.root); return this; }; - _.moveToLeftEnd = function() { return this.moveToDirEnd(L); }; - _.moveToRightEnd = function() { return this.moveToDirEnd(R); }; + moveToLeftEnd () { return this.moveToDirEnd(L); }; + moveToRightEnd () { return this.moveToDirEnd(R); }; - _.keystroke = function(keys, evt) { - var keys = keys.replace(/^\s+|\s+$/g, '').split(/\s+/); + keystroke (keysString:string, evt:KeyboardEvent) { + var keys = keysString.replace(/^\s+|\s+$/g, '').split(/\s+/); for (var i = 0; i < keys.length; i += 1) { this.__controller.keystroke(keys[i], evt || { preventDefault: noop }); } return this; }; - _.typedText = function(text) { + typedText (text:string) { for (var i = 0; i < text.length; i += 1) this.__controller.typedText(text.charAt(i)); return this; }; - _.dropEmbedded = function(pageX, pageY, options) { + dropEmbedded (pageX:number, pageY:number, options:EmbedOptions) { 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); + var cmd = new EmbedNode().setOptions(options); cmd.createLeftOf(this.__controller.cursor); }; - _.setAriaLabel = function(ariaLabel) { + setAriaLabel (ariaLabel:string) { this.__controller.setAriaLabel(ariaLabel); return this; }; - _.getAriaLabel = function () { + getAriaLabel () { return this.__controller.getAriaLabel(); }; - _.setAriaPostLabel = function(ariaPostLabel, timeout) { + setAriaPostLabel (ariaPostLabel:string, timeout:number) { this.__controller.setAriaPostLabel(ariaPostLabel, timeout); return this; }; - _.getAriaPostLabel = function () { + getAriaPostLabel () { return this.__controller.getAriaPostLabel(); }; - _.clickAt = function(clientX, clientY, target) { + clickAt (clientX:number, clientY:number, target:HTMLElement) { target = target || document.elementFromPoint(clientX, clientY); var ctrlr = this.__controller, root = ctrlr.root; if (!jQuery.contains(root.jQ[0], target)) target = root.jQ[0]; @@ -252,13 +320,19 @@ function getInterface(v) { if (ctrlr.blurred) this.focus(); return this; }; - _.ignoreNextMousedown = function(fn) { + ignoreNextMousedown (fn:CursorOptions['ignoreNextMousedown']) { 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; + MQ.EditableField.prototype = EditableField.prototype; + + + var APIClasses:APIClasses = { + AbstractMathQuill, + EditableField + }; /** * Export the API functions that MathQuill-ify an HTML element into API objects @@ -267,12 +341,12 @@ function getInterface(v) { */ for (var kind in API) (function(kind, defAPIClass) { var APIClass = APIClasses[kind] = defAPIClass(APIClasses); - MQ[kind] = function(el, opts) { + MQ[kind] = function(el:HTMLElement, opts:CursorOptions) { var mq = MQ(el); if (mq instanceof APIClass || !el || !el.nodeType) return mq; - var ctrlr = Controller(APIClass.RootBlock(), $(el), Options()); + var ctrlr = new Controller(new APIClass.RootBlock(), $(el), new Options()); ctrlr.KIND_OF_MQ = kind; - return APIClass(ctrlr).__mathquillify(opts, v); + return new APIClass(ctrlr).__mathquillify(opts, v); }; MQ[kind].prototype = APIClass.prototype; }(kind, API[kind])); @@ -287,14 +361,18 @@ MathQuill.noConflict = function() { 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])); +function RootBlockMixin(_:RootBlockMixinInput) { + _.moveOutOf = function (dir:Direction) { this.controller.handle('moveOutOf', dir) } + _.deleteOutOf = function (dir:Direction) { this.controller.handle('deleteOutOf', dir) } + _.selectOutOf = function (dir:Direction) { this.controller.handle('selectOutOf', dir) } + _.upOutOf = function (dir:Direction) { this.controller.handle('upOutOf', dir) } + _.downOutOf = function (dir:Direction) { this.controller.handle('downOutOf', dir) } + _.reflow = function() { this.controller.handle('reflow'); this.controller.handle('edited'); this.controller.handle('edit'); }; } + + diff --git a/src/services/aria.js b/src/services/aria.ts similarity index 62% rename from src/services/aria.js rename to src/services/aria.ts index ea7f43096..3a5fb903a 100755 --- a/src/services/aria.js +++ b/src/services/aria.ts @@ -11,23 +11,25 @@ * Chrome 54+ on Android works reliably with Talkback. ****************************************/ -var Aria = P(function(_) { - _.init = function() { - this.jQ = jQuery([]); // empty element - // Add the alert DOM element only after the page has loaded. - jQuery(document).ready(function() { - var el = '.mq-aria-alert'; - // No matter how many Mathquill instances exist, we only need one alert object to say something. - if (!jQuery(el).length) jQuery('body').append("

"); // make this as noisy as possible in hopes that all modern screen reader/browser combinations will speak when triggered later. - this.jQ = jQuery(el); - }.bind(this)); - this.items = []; - this.msg = ''; +type AriaQueueItem = NodeRef | Fragment | string; + +class Aria { + controller:Controller; + jQ = jQuery(''); + msg = ''; + items:AriaQueueItem[] = []; + + constructor (controller:Controller) { + this.controller = controller; + }; + + setContainer(el:$) { + this.jQ.appendTo(el); }; - _.queue = function(item, shouldDescribe) { - var output = ''; - if (item instanceof Node) { + queue (item:AriaQueueItem, shouldDescribe:boolean = false) { + var output:Fragment | string = ''; + if (item instanceof MQNode) { // Some constructs include verbal shorthand (such as simple fractions and exponents). // Since ARIA alerts relate to moving through interactive content, we don't want to use that shorthand if it exists // since doing so may be ambiguous or confusing. @@ -47,41 +49,37 @@ var Aria = P(function(_) { output = itemMathspeak; } } else { - output = item; + output = item || ''; } this.items.push(output); return this; }; - _.queueDirOf = function(dir) { + queueDirOf (dir:Direction) { prayDirection(dir); return this.queue(dir === L ? 'before' : 'after'); }; - _.queueDirEndOf = function(dir) { + queueDirEndOf (dir:Direction) { prayDirection(dir); return this.queue(dir === L ? 'beginning of' : 'end of'); }; - _.alert = function(t) { + alert (t?:AriaQueueItem) { if (t) this.queue(t); if (this.items.length) { + // To cut down on potential verbiage from multiple Mathquills firing near-simultaneous ARIA alerts, + // update the text of this instance if its container also has keyboard focus. + // If it does not, leave the DOM unchanged but flush the queue regardless. + // Note: updating the msg variable regardless of focus for unit tests. this.msg = this.items.join(' ').replace(/ +(?= )/g,'').trim(); - this.jQ.empty().text(this.msg); + if (this.controller.containerHasFocus()) { + this.jQ.empty().text(this.msg); + } } return this.clear(); }; - _.clear = function() { + clear () { this.items.length = 0; return this; }; -}); - -// We only ever need one instance of the ARIA alert object, and it needs to be easily accessible from all modules. -var aria = Aria(); - -Controller.open(function(_) { - _.aria = aria; - // based on http://www.gh-mathspeak.com/examples/quick-tutorial/ - // and http://www.gh-mathspeak.com/examples/grammar-rules/ - _.exportMathSpeak = function() { return this.root.mathspeak(); }; -}); +}; diff --git a/src/services/exportText.js b/src/services/exportText.ts similarity index 80% rename from src/services/exportText.js rename to src/services/exportText.ts index 343f7af70..56e58b862 100644 --- a/src/services/exportText.js +++ b/src/services/exportText.ts @@ -3,10 +3,10 @@ * As you can see, only half-baked so far. **********************************************/ -Controller.open(function(_, super_) { - _.exportText = function() { +class Controller_exportText extends ControllerBase { + exportText () { return this.root.foldChildren('', function(text, child) { return text + child.text(); }); }; -}); +}; diff --git a/src/services/focusBlur.js b/src/services/focusBlur.ts similarity index 65% rename from src/services/focusBlur.js rename to src/services/focusBlur.ts index 148082a91..e7fba2b50 100644 --- a/src/services/focusBlur.js +++ b/src/services/focusBlur.ts @@ -1,20 +1,26 @@ -Controller.open(function(_) { - this.onNotify(function (e) { - // these try to cover all ways that mathquill can be modified - if (e === 'edit' || e === 'replace' || e === undefined) { - var controller = this.controller; - if (!controller) return; - if (!controller.options.enableDigitGrouping) return; +ControllerBase.onNotify(function (cursor, e) { + // these try to cover all ways that mathquill can be modified + if (e === 'edit' || e === 'replace' || e === undefined) { + var controller = cursor.controller; + if (!controller) return; + if (!controller.options.enableDigitGrouping) return; - // blurred === false means we are focused. blurred === true or - // blurred === undefined means we are not focused. - if (controller.blurred !== false) return; + // TODO - maybe reconsider these 3 states and drop down to only 2 + // + // blurred === false means we are focused. blurred === true or + // blurred === undefined means we are not focused. + if (controller.blurred !== false) return; - controller.disableGroupingForSeconds(1); - } - }); + controller.disableGroupingForSeconds(1); + } +}); - _.disableGroupingForSeconds = function (seconds) { +class Controller_focusBlur extends Controller_exportText { + blurred:boolean; + __disableGroupingTimeout:number; + textareaSelectionTimeout:number; + + disableGroupingForSeconds (seconds:number) { clearTimeout(this.__disableGroupingTimeout); var jQ = this.root.jQ; @@ -28,10 +34,11 @@ Controller.open(function(_) { } } - _.focusBlurEvents = function() { + focusBlurEvents () { var ctrlr = this, root = ctrlr.root, cursor = ctrlr.cursor; - var blurTimeout; - ctrlr.textarea.focus(function() { + var blurTimeout:number; + const textarea = ctrlr.getTextareaOrThrow(); + textarea.focus(function() { ctrlr.updateMathspeak(); ctrlr.blurred = false; clearTimeout(blurTimeout); @@ -48,7 +55,7 @@ Controller.open(function(_) { }).blur(function() { if (ctrlr.textareaSelectionTimeout) { clearTimeout(ctrlr.textareaSelectionTimeout); - ctrlr.textareaSelectionTimeout = undefined; + ctrlr.textareaSelectionTimeout = 0; } ctrlr.disableGroupingForSeconds(0); ctrlr.blurred = true; @@ -68,7 +75,7 @@ Controller.open(function(_) { ctrlr.updateMathspeak(); } function blur() { // not directly in the textarea blur handler so as to be - cursor.hide().parent.blur(); // synchronous with/in the same frame as + cursor.hide().parent.blur(cursor); // synchronous with/in the same frame as ctrlr.container.removeClass('mq-focused'); // clearing/blurring selection $(window).unbind('blur', windowBlur); @@ -77,13 +84,13 @@ Controller.open(function(_) { } } ctrlr.blurred = true; - cursor.hide().parent.blur(); + cursor.hide().parent.blur(cursor); }; - _.unbindFocusBlurEvents = function() { - var ctrlr = this; - ctrlr.textarea.unbind('focus blur'); + unbindFocusBlurEvents () { + var textarea = this.getTextareaOrThrow(); + textarea.unbind('focus blur'); }; -}); +}; /** * TODO: I wanted to move MathBlock::focus and blur here, it would clean @@ -93,8 +100,8 @@ Controller.open(function(_) { * * 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 + * insertion, which if MathBlock::blur becomes MQNode::blur, would add the + * 'blur' CSS class to all MQSymbol's (because .isEmpty() is true for all * of them). * * I'm not even sure there aren't other troublesome calls to .focus() or diff --git a/src/services/keystroke.js b/src/services/keystroke.ts similarity index 52% rename from src/services/keystroke.js rename to src/services/keystroke.ts index fa59223b5..8b4fcb949 100644 --- a/src/services/keystroke.js +++ b/src/services/keystroke.ts @@ -2,15 +2,8 @@ * 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) { + class MQNode extends NodeBase { + keystroke (key:string, e:KeyboardEvent, ctrlr:Controller) { var cursor = ctrlr.cursor; switch (key) { @@ -39,13 +32,13 @@ Node.open(function(_) { // End -> move to the end of the current block. case 'End': ctrlr.notify('move').cursor.insAtRightEnd(cursor.parent); - aria.queue("end of").queue(cursor.parent, true); + ctrlr.aria.queue("end of").queue(cursor.parent, true); break; // Ctrl-End -> move all the way to the end of the root block. case 'Ctrl-End': ctrlr.notify('move').cursor.insAtRightEnd(ctrlr.root); - aria.queue("end of").queue(ctrlr.ariaLabel).queue(ctrlr.root).queue(ctrlr.ariaPostLabel); + ctrlr.aria.queue("end of").queue(ctrlr.ariaLabel).queue(ctrlr.root).queue(ctrlr.ariaPostLabel); break; // Shift-End -> select to the end of the current block. @@ -65,13 +58,13 @@ Node.open(function(_) { // Home -> move to the start of the current block. case 'Home': ctrlr.notify('move').cursor.insAtLeftEnd(cursor.parent); - aria.queue("beginning of").queue(cursor.parent, true); + ctrlr.aria.queue("beginning of").queue(cursor.parent, true); break; // Ctrl-Home -> move all the way to the start of the root block. case 'Ctrl-Home': ctrlr.notify('move').cursor.insAtLeftEnd(ctrlr.root); - aria.queue("beginning of").queue(ctrlr.ariaLabel).queue(ctrlr.root).queue(ctrlr.ariaPostLabel); + ctrlr.aria.queue("beginning of").queue(ctrlr.ariaLabel).queue(ctrlr.root).queue(ctrlr.ariaPostLabel); break; // Shift-Home -> select to the start of the current block. @@ -105,6 +98,7 @@ Node.open(function(_) { } else { ctrlr.selectLeft(); } + break; case 'Shift-Down': if (cursor[R]) { @@ -113,6 +107,7 @@ Node.open(function(_) { else { ctrlr.selectRight(); } + break; case 'Ctrl-Up': break; case 'Ctrl-Down': break; @@ -135,13 +130,13 @@ Node.open(function(_) { // These remaining hotkeys are only of benefit to people running screen readers. case 'Ctrl-Alt-Up': // speak parent block that has focus - if (cursor.parent.parent && cursor.parent.parent instanceof Node) aria.queue(cursor.parent.parent); - else aria.queue('nothing above'); + if (cursor.parent.parent && cursor.parent.parent instanceof MQNode) ctrlr.aria.queue(cursor.parent.parent); + else ctrlr.aria.queue('nothing above'); break; case 'Ctrl-Alt-Down': // speak current block that has focus - if (cursor.parent && cursor.parent instanceof Node) aria.queue(cursor.parent); - else aria.queue('block is empty'); + if (cursor.parent && cursor.parent instanceof MQNode) ctrlr.aria.queue(cursor.parent); + else ctrlr.aria.queue('block is empty'); break; case 'Ctrl-Alt-Left': // speak left-adjacent block @@ -149,11 +144,11 @@ Node.open(function(_) { cursor.parent.parent && cursor.parent.parent.ends && cursor.parent.parent.ends[L] && - cursor.parent.parent.ends[L] instanceof Node + cursor.parent.parent.ends[L] instanceof MQNode ) { - aria.queue(cursor.parent.parent.ends[L]); + ctrlr.aria.queue(cursor.parent.parent.ends[L]); } else { - aria.queue('nothing to the left'); + ctrlr.aria.queue('nothing to the left'); } break; @@ -162,48 +157,64 @@ Node.open(function(_) { cursor.parent.parent && cursor.parent.parent.ends && cursor.parent.parent.ends[R] && - cursor.parent.parent.ends[R] instanceof Node + cursor.parent.parent.ends[R] instanceof MQNode ) { - aria.queue(cursor.parent.parent.ends[R]); + ctrlr.aria.queue(cursor.parent.parent.ends[R]); } else { - aria.queue('nothing to the right'); + ctrlr.aria.queue('nothing to the right'); } break; case 'Ctrl-Alt-Shift-Down': // speak selection - if (cursor.selection) aria.queue(cursor.selection.join('mathspeak', ' ').trim() + ' selected'); - else aria.queue('nothing selected'); + if (cursor.selection) ctrlr.aria.queue(cursor.selection.join('mathspeak', ' ').trim() + ' selected'); + else ctrlr.aria.queue('nothing selected'); break; case 'Ctrl-Alt-=': case 'Ctrl-Alt-Shift-Right': // speak ARIA post label (evaluation or error) - if (ctrlr.ariaPostLabel.length) aria.queue(ctrlr.ariaPostLabel); - else aria.queue('no answer'); + if (ctrlr.ariaPostLabel.length) ctrlr.aria.queue(ctrlr.ariaPostLabel); + else ctrlr.aria.queue('no answer'); break; default: return; } - aria.alert(); + ctrlr.aria.alert(); 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'); }; + moveOutOf (_dir:Direction, _cursor:Cursor, _updown?:'up' | 'down') { pray('overridden or never called on this node'); } // called by Controller::escapeDir, moveDir + moveTowards (_dir:Direction, _cursor:Cursor, _updown?:'up' | 'down') { pray('overridden or never called on this node'); } // called by Controller::moveDir + deleteOutOf (_dir:Direction, _cursor:Cursor) { pray('overridden or never called on this node'); } // called by Controller::deleteDir + deleteTowards (_dir:Direction, _cursor:Cursor) { pray('overridden or never called on this node'); } // called by Controller::deleteDir + unselectInto (_dir:Direction, _cursor:Cursor) { pray('overridden or never called on this node'); } // called by Controller::selectDir + selectOutOf (_dir:Direction, _cursor:Cursor) { pray('overridden or never called on this node'); } // called by Controller::selectDir + selectTowards (_dir:Direction, _cursor:Cursor) { pray('overridden or never called on this node'); } // called by Controller::selectDir +} + +ControllerBase.onNotify(function(cursor:Cursor, e:ControllerEvent) { + if (e === 'move' || e === 'upDown') cursor.show().clearSelection(); }); +optionProcessors.leftRightIntoCmdGoes = function(updown:'up'|'down') { + if (updown && updown !== 'up' && updown !== 'down') { + throw '"up" or "down" required for leftRightIntoCmdGoes option, ' + + 'got "'+updown+'"'; + } + return updown; +}; + + +ControllerBase.onNotify(function(cursor:Cursor, e:ControllerEvent) { if (e !== 'upDown') cursor.upDownCache = {}; }); +ControllerBase.onNotify(function(cursor:Cursor, e:ControllerEvent) { if (e === 'edit') cursor.show().deleteSelection(); }); +ControllerBase.onNotify(function(cursor:Cursor, e:ControllerEvent) { if (e !== 'select') cursor.endSelection(); }); -Controller.open(function(_) { - this.onNotify(function(e) { - if (e === 'move' || e === 'upDown') this.show().clearSelection(); - }); - _.escapeDir = function(dir, key, e) { +class Controller_keystroke extends Controller_focusBlur { + keystroke (key:string, evt:KeyboardEvent) { + this.cursor.parent.keystroke(key, evt, this.getControllerSelf()); + }; + + escapeDir (dir:Direction, _key:string, e:KeyboardEvent) { prayDirection(dir); var cursor = this.cursor; @@ -215,31 +226,24 @@ Controller.open(function(_) { if (cursor.parent === this.root) return; cursor.parent.moveOutOf(dir, cursor); - aria.alert(); + cursor.controller.aria.alert(); 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) { + moveDir (dir:Direction) { prayDirection(dir); var cursor = this.cursor, updown = cursor.options.leftRightIntoCmdGoes; + var cursorDir = cursor[dir]; if (cursor.selection) { - cursor.insDirOf(dir, cursor.selection.ends[dir]); + cursor.insDirOf(dir, cursor.selection.ends[dir] as MQNode); } - else if (cursor[dir]) cursor[dir].moveTowards(dir, cursor, updown); + else if (cursorDir) cursorDir.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); }; + moveLeft () { return this.moveDir(L); }; + moveRight () { return this.moveDir(R); }; /** * moveUp and moveDown have almost identical algorithms: @@ -253,71 +257,94 @@ Controller.open(function(_) { * 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) { + moveUp () { return this.moveUpDown('up'); }; + moveDown () { return this.moveUpDown('down'); }; + moveUpDown (dir:'up'|'down') { + var self = this; 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]); + var dirInto:'upInto' | 'downInto'; + var dirOutOf:'upOutOf' | 'downOutOf'; + + if (dir === 'up') { + dirInto = 'upInto'; + dirOutOf = 'upOutOf'; + } else { + dirInto = 'downInto'; + dirOutOf = 'downOutOf'; + } + + var cursorL = cursor[L]; + var cursorR = cursor[R]; + var cursorR_dirInto = cursorR && cursorR[dirInto]; + var cursorL_dirInto = cursorL && cursorL[dirInto]; + + if (cursorR_dirInto) cursor.insAtLeftEnd(cursorR_dirInto); + else if (cursorL_dirInto) cursor.insAtRightEnd(cursorL_dirInto); else { - cursor.parent.bubble(function(ancestor) { + cursor.parent.bubble(function(ancestor:MQNode) { // TODO - revist this 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; + if (typeof prop === 'function') prop = prop.call(ancestor,cursor) as any; // TODO - figure out if we need to assign to prop + if (prop instanceof MQNode) cursor.jumpUpDown(ancestor, prop); + if (prop as any !== true) return false; // TODO - figure out how this can return true } + return undefined; }); } return self; } - this.onNotify(function(e) { if (e !== 'upDown') this.upDownCache = {}; }); - - this.onNotify(function(e) { if (e === 'edit') this.show().deleteSelection(); }); - _.deleteDir = function(dir) { + deleteDir (dir:Direction) { prayDirection(dir); var cursor = this.cursor; - var cursorEl = cursor[dir], cursorElParent = cursor.parent.parent; - if(cursorEl && cursorEl instanceof Node) { - if(cursorEl.sides) { - aria.queue(cursorEl.parent.chToCmd(cursorEl.sides[-dir].ch).mathspeak({createdLeftOf: cursor})); + var cursorEl = cursor[dir] as MQNode; + var cursorElParent = cursor.parent.parent; + var ctrlr = cursor.controller; + + if(cursorEl && cursorEl instanceof MQNode) { + if(cursorEl.sides ) { + ctrlr.aria.queue(cursorEl.parent.chToCmd(cursorEl.sides[-dir as Direction].ch).mathspeak({createdLeftOf: cursor})); // generally, speak the current element if it has no blocks, // but don't for text block commands as the deleteTowards method // in the TextCommand class is responsible for speaking the new character under the cursor. } else if (!cursorEl.blocks && cursorEl.parent.ctrlSeq !== '\\text') { - aria.queue(cursorEl); + ctrlr.aria.queue(cursorEl); } - } else if(cursorElParent && cursorElParent instanceof Node) { + } else if(cursorElParent && cursorElParent instanceof MQNode) { if(cursorElParent.sides) { - aria.queue(cursorElParent.parent.chToCmd(cursorElParent.sides[dir].ch).mathspeak({createdLeftOf: cursor})); + ctrlr.aria.queue(cursorElParent.parent.chToCmd(cursorElParent.sides[dir].ch).mathspeak({createdLeftOf: cursor})); } else if (cursorElParent.blocks && cursorElParent.mathspeakTemplate) { if (cursorElParent.upInto && cursorElParent.downInto) { // likely a fraction, and we just backspaced over the slash - aria.queue(cursorElParent.mathspeakTemplate[1]); + ctrlr.aria.queue(cursorElParent.mathspeakTemplate[1]); } else { var mst = cursorElParent.mathspeakTemplate; var textToQueue = dir === L ? mst[0] : mst[mst.length - 1]; - aria.queue(textToQueue); + ctrlr.aria.queue(textToQueue); } } else { - aria.queue(cursorElParent); + ctrlr.aria.queue(cursorElParent); } } var hadSelection = cursor.selection; this.notify('edit'); // deletes selection if present if (!hadSelection) { - if (cursor[dir]) cursor[dir].deleteTowards(dir, cursor); + const cursorDir = cursor[dir]; + if (cursorDir) cursorDir.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(function (node) { node.reflow(); }); + const cursorL = cursor[L] as MQNode; + const cursorR = cursor[R] as MQNode; + if (cursorL.siblingDeleted) cursorL.siblingDeleted(cursor.options, R); + if (cursorR.siblingDeleted) cursorR.siblingDeleted(cursor.options, L); + cursor.parent.bubble(function (node) { + (node as MQNode).reflow(); + return undefined; + }); return this; }; - _.ctrlDeleteDir = function(dir) { + ctrlDeleteDir (dir:Direction) { prayDirection(dir); var cursor = this.cursor; if (!cursor[dir] || cursor.selection) return this.deleteDir(dir); @@ -325,26 +352,30 @@ Controller.open(function(_) { this.notify('edit'); var fragRemoved; if (dir === L) { - fragRemoved = Fragment(cursor.parent.ends[L], cursor[L]); + fragRemoved = new Fragment((cursor.parent as MQNode).ends[L] as MQNode, cursor[L] as MQNode); } else { - fragRemoved = Fragment(cursor[R], cursor.parent.ends[R]); + fragRemoved = new Fragment(cursor[R] as MQNode, (cursor.parent as MQNode).ends[R] as MQNode); } - aria.queue(fragRemoved); + cursor.controller.aria.queue(fragRemoved); fragRemoved.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(function (node) { node.reflow(); }); + const cursorL = cursor[L]; + const cursorR = cursor[R]; + if (cursorL) cursorL.siblingDeleted(cursor.options, R); + if (cursorR) cursorR.siblingDeleted(cursor.options, L); + cursor.parent.bubble(function (node) { + (node as MQNode).reflow(); + return undefined; + }); return this; }; - _.backspace = function() { return this.deleteDir(L); }; - _.deleteForward = function() { return this.deleteDir(R); }; + backspace () { return this.deleteDir(L); }; + deleteForward () { return this.deleteDir(R); }; - this.onNotify(function(e) { if (e !== 'select') this.endSelection(); }); - _.selectDir = function(dir) { + selectDir (dir:Direction) { var cursor = this.notify('select').cursor, seln = cursor.selection; prayDirection(dir); @@ -355,7 +386,7 @@ Controller.open(function(_) { // "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) { + if (seln && seln.ends[dir] === node && (cursor.anticursor as Anticursor)[-dir as Direction] !== node) { node.unselectInto(dir, cursor); } else node.selectTowards(dir, cursor); @@ -364,8 +395,11 @@ Controller.open(function(_) { cursor.clearSelection(); cursor.select() || cursor.show(); - if (cursor.selection) aria.clear().queue(cursor.selection.join('mathspeak', ' ').trim() + ' selected'); // clearing first because selection fires several times, and we don't want repeated speech. + var selection = cursor.selection; + if (selection) { + cursor.controller.aria.clear().queue(selection.join('mathspeak', ' ').trim() + ' selected'); // clearing first because selection fires several times, and we don't want repeated speech. + } }; - _.selectLeft = function() { return this.selectDir(L); }; - _.selectRight = function() { return this.selectDir(R); }; -}); + selectLeft () { return this.selectDir(L); }; + selectRight () { return this.selectDir(R); }; +}; diff --git a/src/services/latex.js b/src/services/latex.ts similarity index 73% rename from src/services/latex.js rename to src/services/latex.ts index 432c93261..b6cbc0346 100644 --- a/src/services/latex.js +++ b/src/services/latex.ts @@ -1,12 +1,18 @@ +class TempSingleCharNode extends MQNode { + constructor (_char:string) { + super(); + } +} + // Parser MathBlock var latexMathParser = (function() { - function commandToBlock(cmd) { // can also take in a Fragment - var block = MathBlock(); + function commandToBlock(cmd:MQNode | Fragment):MathBlock { // can also take in a Fragment + var block = new MathBlock(); cmd.adopt(block, 0, 0); return block; } - function joinBlocks(blocks) { - var firstBlock = blocks[0] || MathBlock(); + function joinBlocks(blocks:MathBlock[]) { + var firstBlock = blocks[0] || new MathBlock(); for (var i = 1; i < blocks.length; i += 1) { blocks[i].children().adopt(firstBlock, firstBlock.ends[R], 0); @@ -26,9 +32,9 @@ var latexMathParser = (function() { // 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 number = digit.map(function (c) { return Digit(c); }); - var symbol = regex(/^[^${}\\_^]/).map(function(c) { return VanillaSymbol(c); }); + var variable = letter.map(function(c) { return new Letter(c); }); + var number = digit.map(function (c) { return new Digit(c); }); + var symbol = regex(/^[^${}\\_^]/).map(function(c) { return new VanillaSymbol(c); }); var controlSequence = regex(/^[^\\a-eg-zA-Z]/) // hotfix #164; match MathBlock::write @@ -36,11 +42,18 @@ var latexMathParser = (function() { regex(/^[a-z]+/i) .or(regex(/^\s+/).result(' ')) .or(any) - )).then(function(ctrlSeq) { - var cmdKlass = LatexCmds[ctrlSeq]; + )) + .then(function(ctrlSeq):Parser { // TODO - is Parser correct? + var cmdKlass = (LatexCmds as LatexCmdsSingleChar)[ctrlSeq]; if (cmdKlass) { - return cmdKlass(ctrlSeq).parser(); + if (cmdKlass.constructor) { + var actualClass = cmdKlass as typeof TempSingleCharNode; // TODO - figure out how to know the difference + return new actualClass(ctrlSeq).parser(); + } else { + var builder = cmdKlass as (c:string) => TempSingleCharNode; // TODO - figure out how to know the difference + return builder(ctrlSeq).parser(); + } } else { return fail('unknown command: \\'+ctrlSeq); @@ -56,46 +69,50 @@ var latexMathParser = (function() { ; // Parsers yielding MathBlocks - var mathGroup = string('{').then(function() { return mathSequence; }).skip(string('}')); + var mathGroup:Parser= 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(); + return block.join('latex') !== ']' ? succeed(block) : fail(''); }) .many().map(joinBlocks).skip(optWhitespace) ).skip(string(']')) ; - var latexMath = mathSequence; + var latexMath:typeof mathSequence & { + block: typeof mathBlock; + optBlock: typeof optMathBlock + } = mathSequence as any; latexMath.block = mathBlock; latexMath.optBlock = optMathBlock; return latexMath; })(); -Controller.open(function(_, super_) { - _.cleanLatex = function (latex) { + +optionProcessors.maxDepth = function(depth:number) { + return (typeof depth === 'number') ? depth : undefined; +}; + +class Controller_latex extends Controller_keystroke { + cleanLatex (latex:string) { //prune unnecessary spaces return latex.replace(/(\\[a-z]+) (?![a-z])/ig,'$1') } - _.exportLatex = function() { + exportLatex () { return this.cleanLatex(this.root.latex()); }; - - optionProcessors.maxDepth = function(depth) { - return (typeof depth === 'number') ? depth : undefined; - }; - _.writeLatex = function(latex) { + writeLatex (latex:string) { var cursor = this.notify('edit').cursor; cursor.parent.writeLatex(cursor, latex); return this; }; - _.classifyLatexForEfficientUpdate = function (latex) { + classifyLatexForEfficientUpdate (latex:string) { if (typeof latex !== 'string') return; var matches = latex.match(/-?[0-9.]+$/g); @@ -106,8 +123,10 @@ Controller.open(function(_, super_) { digits: matches[0] }; } + + return; }; - _.renderLatexMathEfficiently = function (latex) { + renderLatexMathEfficiently (latex:string) { var root = this.root; var oldLatex = this.exportLatex(); if (root.ends[L] && root.ends[R] && oldLatex === latex) { @@ -144,7 +163,7 @@ Controller.open(function(_, super_) { var oldCharNodes = []; for (var i= oldDigits.length - 1; i >= 0; i--) { // the tree does not match what we expect - if (charNode.ctrlSeq !== oldDigits[i]) { + if (!charNode || charNode.ctrlSeq !== oldDigits[i]) { return false; } @@ -165,27 +184,31 @@ Controller.open(function(_, super_) { // remove the minus sign if (oldMinusSign && !newMinusSign) { var oldMinusNode = charNode; + if (!oldMinusNode) return false; if (oldMinusNode.ctrlSeq !== '-') return false; if (oldMinusNode[R] !== oldCharNodes[0]) return false; if (oldMinusNode.parent !== root) return false; - if (oldMinusNode[L] && oldMinusNode[L].parent !== root) return false; + + const oldMinusNodeL = oldMinusNode[L]; + if (oldMinusNodeL && oldMinusNodeL.parent !== root) return false; oldCharNodes[0][L] = oldMinusNode[L]; if (root.ends[L] === oldMinusNode) root.ends[L] = oldCharNodes[0]; - if (oldMinusNode[L]) oldMinusNode[L][R] = oldCharNodes[0]; + if (oldMinusNodeL) oldMinusNodeL[R] = oldCharNodes[0]; oldMinusNode.jQ.remove(); } // add a minus sign if (!oldMinusSign && newMinusSign) { - var newMinusNode = PlusMinus('-'); + var newMinusNode = new PlusMinus('-'); var minusSpan = document.createElement('span'); minusSpan.textContent = '-'; newMinusNode.jQ = $(minusSpan); - if (oldCharNodes[0][L]) oldCharNodes[0][L][R] = newMinusNode; + var oldCharNodes0L = oldCharNodes[0][L]; + if (oldCharNodes0L) oldCharNodes0L[R] = newMinusNode; if (root.ends[L] === oldCharNodes[0]) root.ends[L] = newMinusNode; newMinusNode.parent = root; @@ -193,7 +216,7 @@ Controller.open(function(_, super_) { newMinusNode[R] = oldCharNodes[0]; oldCharNodes[0][L] = newMinusNode; - newMinusNode.contactWeld(); // decide if binary operator + newMinusNode.contactWeld(this.cursor); // decide if binary operator newMinusNode.jQ.insertBefore(oldCharNodes[0].jQ); } @@ -229,7 +252,7 @@ Controller.open(function(_, super_) { span.className = "mq-digit"; span.textContent = newDigits[i]; - var newNode = Digit(newDigits[i]); + var newNode = new Digit(newDigits[i]); newNode.parent = root; newNode.jQ = $(span); frag.appendChild(span); @@ -237,7 +260,9 @@ Controller.open(function(_, super_) { // splice this node in newNode[L] = root.ends[R]; newNode[R] = 0; - newNode[L][R] = newNode; + + const newNodeL = newNode[L] as MQNode; + newNodeL[R] = newNode; root.ends[R] = newNode; } @@ -253,18 +278,18 @@ Controller.open(function(_, super_) { this.cursor.resetToEnd(this); var rightMost = root.ends[R]; - if (rightMost.fixDigitGrouping) { + if (rightMost) { rightMost.fixDigitGrouping(this.cursor.options); } return true; }; - _.renderLatexMathFromScratch = function (latex) { + renderLatexMathFromScratch (latex:string) { 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); + var block = latexMathParser.skip(eof).or(all.result(false)).parse(latex); root.ends[L] = root.ends[R] = 0; @@ -278,7 +303,7 @@ Controller.open(function(_, super_) { var html = block.join('html'); jQ.html(html); root.jQize(jQ.children()); - root.finalizeInsert(cursor.options); + root.finalizeInsert(cursor.options, cursor); } else { jQ.empty(); } @@ -286,13 +311,13 @@ Controller.open(function(_, super_) { delete cursor.selection; cursor.insAtRightEnd(root); }; - _.renderLatexMath = function(latex) { + renderLatexMath (latex:string) { this.notify('replace'); if (this.renderLatexMathEfficiently(latex)) return; this.renderLatexMathFromScratch(latex); }; - _.renderLatexText = function(latex) { + renderLatexText (latex:string) { var root = this.root, cursor = this.cursor; root.jQ.children().slice(1).remove(); @@ -313,20 +338,20 @@ Controller.open(function(_, super_) { .skip(string('$').or(eof)) .map(function(block) { // HACK FIXME: this shouldn't have to have access to cursor - var rootMathCommand = RootMathCommand(cursor); + var rootMathCommand = new RootMathCommand(cursor); rootMathCommand.createBlocks(); var rootMathBlock = rootMathCommand.ends[L]; - block.children().adopt(rootMathBlock, 0, 0); + block.children().adopt(rootMathBlock as MQNode, 0, 0); return rootMathCommand; }) ; var escapedDollar = string('\\$').result('$'); - var textChar = escapedDollar.or(regex(/^[^$]/)).map(VanillaSymbol); + var textChar = escapedDollar.or(regex(/^[^$]/)).map((ch) => new VanillaSymbol(ch)); var latexText = mathMode.or(textChar).many(); - var commands = latexText.skip(eof).or(all.result(false)).parse(latex); + var commands = latexText.skip(eof).or(all.result(false)).parse(latex); if (commands) { for (var i = 0; i < commands.length; i += 1) { @@ -335,7 +360,7 @@ Controller.open(function(_, super_) { root.jQize().appendTo(root.jQ); - root.finalizeInsert(cursor.options); + root.finalizeInsert(cursor.options, cursor); } }; -}); +}; diff --git a/src/services/mouse.js b/src/services/mouse.ts similarity index 65% rename from src/services/mouse.js rename to src/services/mouse.ts index b6d4b8b96..5a9d84f99 100644 --- a/src/services/mouse.js +++ b/src/services/mouse.ts @@ -1,70 +1,80 @@ /******************************************************** * Deals with mouse events for clicking, drag-to-select *******************************************************/ - -Controller.open(function(_) { - Options.p.ignoreNextMousedown = noop; - - // Whenever edits to the tree occur, in-progress selection events - // must be invalidated and selection changes must not be applied to - // the edited tree. cancelSelectionOnEdit takes care of this. - var cancelSelectionOnEdit; - this.onNotify(function (e) { +const ignoreNextMouseDownNoop = (_el:MouseEvent) => { return false }; +Options.prototype.ignoreNextMousedown = ignoreNextMouseDownNoop; + +// Whenever edits to the tree occur, in-progress selection events +// must be invalidated and selection changes must not be applied to +// the edited tree. cancelSelectionOnEdit takes care of this. +var cancelSelectionOnEdit: undefined | { + cb: () => void, + cursor: Cursor +}; + +(function () { + ControllerBase.onNotify(function (cursor, e) { if ((e === 'edit' || e === 'replace')) { // this will be called any time ANY mathquill is edited. We only want // to cancel selection if the selection is happening within the mathquill // that dispatched the notify. Otherwise you won't be able to select any // mathquills while a slider is playing. - if (cancelSelectionOnEdit && cancelSelectionOnEdit.cursor === this) { + if (cancelSelectionOnEdit && cancelSelectionOnEdit.cursor === cursor) { cancelSelectionOnEdit.cb(); } } }); +})(); - _.delegateMouseEvents = function() { +class Controller_mouse extends Controller_latex { + delegateMouseEvents () { var ultimateRootjQ = this.root.jQ; //drag-to-select event handling - this.container.bind('mousedown.mathquill', function(e) { + this.container.bind('mousedown.mathquill', function(_e:Event) { + var e = _e as MouseEvent; var rootjQ = $(e.target).closest('.mq-root-block'); - var root = Node.getNodeOfElement(rootjQ[0]) || Node.getNodeOfElement(ultimateRootjQ[0]); + var root = (NodeBase.getNodeOfElement(rootjQ[0]) || NodeBase.getNodeOfElement(ultimateRootjQ[0])) as ControllerRoot; var ctrlr = root.controller, cursor = ctrlr.cursor, blink = cursor.blink; - var textareaSpan = ctrlr.textareaSpan, textarea = ctrlr.textarea; + var textareaSpan = ctrlr.getTextareaSpanOrThrow(); + var textarea = ctrlr.getTextareaOrThrow(); e.preventDefault(); // doesn't work in IE≤8, but it's a one-line fix: - e.target.unselectable = true; // http://jsbin.com/yagekiji/1 + (e.target as any).unselectable = true; // http://jsbin.com/yagekiji/1 // TODO - no idea what this unselectable property is if (cursor.options.ignoreNextMousedown(e)) return; - else cursor.options.ignoreNextMousedown = noop; + else cursor.options.ignoreNextMousedown = ignoreNextMouseDownNoop; - var target; - function mousemove(e) { target = $(e.target); } - function docmousemove(e) { + var target:$ | undefined; + function mousemove(e:Event) { target = $(e.target); } + function docmousemove(e:MouseEvent) { if (!cursor.anticursor) cursor.startSelection(); - ctrlr.seek(target, e.pageX, e.pageY).cursor.select(); - if(cursor.selection) aria.clear().queue(cursor.selection.join('mathspeak') + ' selected').alert(); + ctrlr.seek(target!, e.pageX, e.pageY).cursor.select(); + if(cursor.selection) cursor.controller.aria.clear().queue(cursor.selection.join('mathspeak') + ' selected').alert(); 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 unbindListeners (e) { + function unbindListeners (e:MouseEvent) { // delete the mouse handlers now that we're not dragging anymore rootjQ.unbind('mousemove', mousemove); - $(e.target.ownerDocument).unbind('mousemove', docmousemove).unbind('mouseup', mouseup); + + const anyTarget = e.target as any; // TODO - why do we need to cast to any? + $(anyTarget.ownerDocument).unbind('mousemove', docmousemove).unbind('mouseup', mouseup); cancelSelectionOnEdit = undefined; } function updateCursor () { if (ctrlr.editable) { cursor.show(); - aria.queue(cursor.parent).alert(); + cursor.controller.aria.queue(cursor.parent).alert(); } else { textareaSpan.detach(); } } - function mouseup(e) { + function mouseup(e:MouseEvent) { cursor.blink = blink; if (!cursor.selection) updateCursor(); unbindListeners(e); @@ -99,25 +109,24 @@ Controller.open(function(_) { ctrlr.seek($(e.target), e.pageX, e.pageY).cursor.startSelection(); rootjQ.mousemove(mousemove); - $(e.target.ownerDocument).mousemove(docmousemove).mouseup(mouseup); + const anyTarget = e.target as any; // TODO - why do we need to cast to any? + $(anyTarget.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) { + + seek ($target:$, pageX:number, _pageY:number) { var cursor = this.notify('select').cursor; var node; - var targetElm = $target && $target[0]; + var targetElm:HTMLElement | null = $target && $target[0]; // we can click on an element that is deeply nested past the point // that mathquill knows about. We need to traverse up to the first // node that mathquill is aware of while (targetElm) { // try to find the MQ Node associated with the DOM Element - node = Node.getNodeOfElement(targetElm); + node = NodeBase.getNodeOfElement(targetElm); if (node) break; // must be too deep, traverse up to the parent DOM Element @@ -139,4 +148,4 @@ Controller.open(function(_) { // always hits no-selection case in scrollHoriz and scrolls slower return this; }; -}); +}; diff --git a/src/services/parser.util.js b/src/services/parser.util.js deleted file mode 100644 index db76ed5ae..000000000 --- a/src/services/parser.util.js +++ /dev/null @@ -1,205 +0,0 @@ -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); - }); -}); diff --git a/src/services/parser.util.ts b/src/services/parser.util.ts new file mode 100644 index 000000000..f99f2e846 --- /dev/null +++ b/src/services/parser.util.ts @@ -0,0 +1,220 @@ + +function parseError(stream: string, message: string): never { + if (stream) { + stream = "'"+stream+"'"; + } + else { + stream = 'EOF'; + } + + throw 'Parse Error: '+message+' at '+stream; +} + +type UnknownParserResult = any; + +type ParserBody = ( + stream: string, + onSuccess: (stream: string, result: T) => UnknownParserResult, + onFailure: (stream: string, msg: string) => UnknownParserResult +) => T; + + +class Parser { + _: ParserBody; + + // 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. + constructor (body: ParserBody) { + this._ = body; + } + + parse (stream: string): T { + return this.skip(Parser.eof)._(''+stream, success, parseError); + + function success(_stream: string, result: T) { return result; } + }; + + // -*- primitive combinators -*- // + or (alternative: Parser): Parser { + pray('or is passed a parser', alternative instanceof Parser); + + var self = this; + + return new Parser(function(stream, onSuccess, onFailure) { + return self._(stream, onSuccess, failure); + + function failure(_newStream: string) { + return alternative._(stream, onSuccess, onFailure); + } + }); + }; + + then (next:Parser|((result: T)=>Parser)):Parser { + var self = this; + + return new Parser(function(stream: string, onSuccess, onFailure) { + return self._(stream, success, onFailure) as any as Q; + + function success(newStream: string, result: T) { + 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 (): Parser { + var self = this; + + return new Parser(function(stream, onSuccess, _onFailure) { + var xs: T[] = []; + while (self._(stream, success, failure)); + return onSuccess(stream, xs); + + function success(newStream: string, x: T) { + stream = newStream; + xs.push(x); + return true; + } + + function failure() { + return false; + } + }); + }; + + times (min: number, max?: number): Parser { + if (arguments.length < 2) max = min; + var self = this; + + return new Parser(function(stream, onSuccess, onFailure) { + var xs: T[] = []; + var result: boolean = true; + var failure; + + for (var i = 0; i < min; i += 1) { + // TODO, this may be incorrect for parsers that return boolean + // (or generally, falsey) values + result = !!self._(stream, success, firstFailure); + if (!result) return onFailure(stream, failure as any as string); + } + + for (; i < (max as number) && result; i += 1) { + self._(stream, success, secondFailure); + } + + return onSuccess(stream, xs); + + function success(newStream: string, x: T) { + xs.push(x); + stream = newStream; + return true; + } + + function firstFailure(newStream: string, msg: string) { + failure = msg; + stream = newStream; + return false; + } + + function secondFailure(_newStream: string, _msg: string) { + return false; + } + }); + }; + + // -*- higher-level combinators -*- // + result (res: Q): Parser { return this.then(Parser.succeed(res)); }; + atMost (n: number) { return this.times(0, n); }; + atLeast (n: number) { + var self = this; + return self.times(n).then(function(start) { + return self.many().map(function(end) { + return start.concat(end); + }); + }); + }; + + map (fn: (result: T)=>Q): Parser { + return this.then(function(result) { return Parser.succeed(fn(result)); }); + }; + + skip (two: Parser): Parser { + return this.then(function(result) { return two.result(result); }); + }; + + // -*- primitive parsers -*- // + static string (str: string): Parser { + var len = str.length; + var expected = "expected '"+str+"'"; + + return new 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); + } + }); + }; + + static regex (re: RegExp): Parser { + pray('regexp parser is anchored', re.toString().charAt(1) === '^'); + + var expected = 'expected '+re; + + return new 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); + } + }); + }; + + static succeed (result: Q): Parser { + return new Parser(function(stream: string, onSuccess) { + return onSuccess(stream, result); + }); + }; + + static fail (msg: string):Parser { + return new Parser(function(stream, _, onFailure) { + return onFailure(stream, msg) as never; + }); + }; + + static letter = Parser.regex(/^[a-z]/i); + static letters = Parser.regex(/^[a-z]*/i); + static digit = Parser.regex(/^[0-9]/); + static digits = Parser.regex(/^[0-9]*/); + static whitespace = Parser.regex(/^\s+/); + static optWhitespace = Parser.regex(/^\s*/); + + static any: Parser = new Parser(function(stream, onSuccess, onFailure) { + if (!stream) return onFailure(stream, 'expected any character'); + + return onSuccess(stream.slice(1), stream.charAt(0)); + }); + + static all: Parser = new Parser(function(stream, onSuccess, _onFailure) { + return onSuccess('', stream); + }); + + static eof: Parser = new Parser(function(stream, onSuccess, onFailure) { + if (stream) return onFailure(stream, 'expected EOF'); + + return onSuccess(stream, stream); + }); +}; diff --git a/src/services/saneKeyboardEvents.util.js b/src/services/saneKeyboardEvents.util.ts similarity index 90% rename from src/services/saneKeyboardEvents.util.js rename to src/services/saneKeyboardEvents.util.ts index 4238a0107..32ed871c1 100644 --- a/src/services/saneKeyboardEvents.util.js +++ b/src/services/saneKeyboardEvents.util.ts @@ -20,6 +20,7 @@ * + event handler logic * + attach event handlers and export methods ************************************************/ +type TextareaChecker = ((e?:Event) => void); var saneKeyboardEvents = (function() { // The following [key values][1] map was compiled from the @@ -31,7 +32,7 @@ var saneKeyboardEvents = (function() { // [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 = { + const KEY_VALUES:Record = { 8: 'Backspace', 9: 'Tab', @@ -67,7 +68,7 @@ var saneKeyboardEvents = (function() { // To the extent possible, create a normalized string representation // of the key combo (i.e., key code and modifier keys). - function stringify(evt) { + function stringify(evt:JQ_KeyboardEvent) { var which = evt.which || evt.keyCode; var keyVal = KEY_VALUES[which]; var key; @@ -88,9 +89,9 @@ var saneKeyboardEvents = (function() { // create a keyboard events shim that calls callbacks at useful times // and exports useful public methods - return function saneKeyboardEvents(el, controller) { - var keydown = null; - var keypress = null; + return function saneKeyboardEvents(el:$, controller:Controller) { + var keydown:JQ_KeyboardEvent | null = null; + var keypress:KeyboardEvent | null = null; var textarea = jQuery(el); var target = jQuery(controller.container || textarea); @@ -103,20 +104,21 @@ var saneKeyboardEvents = (function() { // 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) { + var checkTextarea:TextareaChecker = noop; + var timeoutId:number; + function checkTextareaFor(checker:TextareaChecker) { checkTextarea = checker; clearTimeout(timeoutId); timeoutId = setTimeout(checker); } - function checkTextareaOnce(checker) { + function checkTextareaOnce(checker:TextareaChecker) { checkTextareaFor(function(e) { checkTextarea = noop; clearTimeout(timeoutId); checker(e); }); } - target.bind('keydown keypress input keyup paste', function(e) { + target.bind('keydown keypress input keyup paste', function(e:KeyboardEvent) { checkTextarea(e); }); @@ -132,7 +134,7 @@ var saneKeyboardEvents = (function() { } // -*- public methods -*- // - function select(text) { + function select(text:string) { // 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) @@ -160,20 +162,20 @@ var saneKeyboardEvents = (function() { function handleKey() { if (controller.options && controller.options.overrideKeystroke) { - controller.options.overrideKeystroke(stringify(keydown), keydown); + controller.options.overrideKeystroke(stringify(keydown!), keydown!); } else { - controller.keystroke(stringify(keydown), keydown); + controller.keystroke(stringify(keydown!), keydown!); } } // -*- event handlers -*- // - function onKeydown(e) { + function onKeydown(e:KeyboardEvent) { if (e.target !== textarea[0]) return; keydown = e; keypress = null; - if (shouldBeSelected) checkTextareaOnce(function(e) { + if (shouldBeSelected) checkTextareaOnce(function(e?:Event) { if (!(e && e.type === 'focusout')) { // re-select textarea in case it's an unrecognized key that clears // the selection, then never again, 'cos next thing might be blur @@ -184,7 +186,7 @@ var saneKeyboardEvents = (function() { handleKey(); } - function isArrowKey (e) { + function isArrowKey (e:JQ_KeyboardEvent) { if (!e || !e.originalEvent) return false; // The keyPress event in FF reports which=0 for some reason. The new @@ -199,7 +201,7 @@ var saneKeyboardEvents = (function() { return false; } - function onKeypress(e) { + function onKeypress(e:KeyboardEvent) { if (e.target !== textarea[0]) return; // call the key handler for repeated keypresses. @@ -218,7 +220,7 @@ var saneKeyboardEvents = (function() { checkTextareaFor(typedText); } } - function onKeyup(e) { + function onKeyup(e:KeyboardEvent) { if (e.target !== textarea[0]) return; // Handle case of no keypress event being sent @@ -274,7 +276,7 @@ var saneKeyboardEvents = (function() { textarea.val(''); } - function onPaste(e) { + function onPaste(e:Event) { if (e.target !== textarea[0]) return; // browsers are dumb. @@ -309,9 +311,9 @@ var saneKeyboardEvents = (function() { keypress: onKeypress, keyup: onKeyup, focusout: onBlur, - copy: function(e) { e.preventDefault(); }, - cut: function(e) { e.preventDefault(); }, - paste: function(e) { e.preventDefault(); } + copy: function(e:Event) { e.preventDefault(); }, + cut: function(e:Event) { e.preventDefault(); }, + paste: function(e:Event) { e.preventDefault(); } }); } else { target.bind({ diff --git a/src/services/scrollHoriz.js b/src/services/scrollHoriz.ts similarity index 90% rename from src/services/scrollHoriz.js rename to src/services/scrollHoriz.ts index bc7afb4cb..3075a402b 100644 --- a/src/services/scrollHoriz.js +++ b/src/services/scrollHoriz.ts @@ -3,8 +3,8 @@ * overflow their width **********************************************/ -Controller.open(function(_) { - _.setOverflowClasses = function () { +class Controller_scrollHoriz extends Controller_mouse { + setOverflowClasses () { var root = this.root.jQ[0]; var shouldHaveOverflowRight = false; var shouldHaveOverflowLeft = false; @@ -20,13 +20,13 @@ Controller.open(function(_) { if (root.classList.contains('mq-editing-overflow-left') !== shouldHaveOverflowLeft) root.classList.toggle('mq-editing-overflow-left') } - _.scrollHoriz = function() { + scrollHoriz () { var cursor = this.cursor, seln = cursor.selection; var rootRect = this.root.jQ[0].getBoundingClientRect(); if (!cursor.jQ[0] && !seln) { - this.root.jQ.stop().animate({scrollLeft: 0}, 100, function () { + this.root.jQ.stop().animate({scrollLeft: 0}, 100, () => { this.setOverflowClasses(); - }.bind(this)); + }); return; } else if (!seln) { var x = cursor.jQ[0].getBoundingClientRect().left; @@ -58,8 +58,8 @@ Controller.open(function(_) { var root = this.root.jQ[0] if (scrollBy < 0 && root.scrollLeft === 0) return if (scrollBy > 0 && root.scrollWidth <= root.scrollLeft + rootRect.width) return - this.root.jQ.stop().animate({ scrollLeft: '+=' + scrollBy}, 100, function () { + this.root.jQ.stop().animate({ scrollLeft: '+=' + scrollBy}, 100, () => { this.setOverflowClasses(); - }.bind(this)); + }); }; -}); +}; diff --git a/src/services/textarea.js b/src/services/textarea.ts similarity index 75% rename from src/services/textarea.js rename to src/services/textarea.ts index 78f04d102..aa23be57a 100644 --- a/src/services/textarea.js +++ b/src/services/textarea.ts @@ -2,24 +2,28 @@ * Manage the MathQuill instance's textarea * (as owned by the Controller) ********************************************/ +Options.prototype.substituteTextarea = function() { + return $('