From 81520d812d5badf01be2f0bd36ec18b18080e054 Mon Sep 17 00:00:00 2001 From: Stephen Clower Date: Wed, 24 Feb 2016 15:48:30 -0500 Subject: [PATCH 001/393] Adding a class to enable support for creating ARIA alerts for use in communicating feedback when navigating MQ textboxes for screen reader users. --- src/services/aria.js | 45 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/services/aria.js diff --git a/src/services/aria.js b/src/services/aria.js new file mode 100644 index 000000000..b32fbc41d --- /dev/null +++ b/src/services/aria.js @@ -0,0 +1,45 @@ +/***************************************** + + * Add the capability for mathquill to generate ARIA alerts. Necessary so MQ can convey information as a screen reader user navigates the fake MathQuill textareas. + * Official ARIA specification: https://www.w3.org/TR/wai-aria/ + * WAI-ARIA is still catching on, thus only more recent browsers support it, and even then to varying degrees. + * The below implementation attempts to be as broad as possible and may not conform precisely to the spec. But, neither do any browsers or adaptive technologies at this point. + * At time of writing, IE 11, FF 44, and Safari 8.0.8 work. Older versions of these browsers should speak as well, but haven't tested precisely which earlier editions pass. + * Todo: find out why Chrome refuses to speak. + + * Tested AT: on Windows, Window-Eyes, ZoomText Fusion, NVDA, and JAWS (all supported). + * VoiceOver on Mac platforms also supported (only tested with OSX 10.10.5 and iOS 9.2.1+). + * Android is hit or miss, Firefox seems to work more predictably than Chrome when tested against Talkback. + ****************************************/ + +var Aria = P(function(_) { + _.init = 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); + this.text = ""; + }; + + _.queue = function(t, shouldAppend) { + var spaceChar = " "; + if(this.text === "" || t === "") spaceChar = ""; + if(t) { + if (shouldAppend) { + this.text = this.text + spaceChar + t; + } + else { + this.text = t + spaceChar + this.text; + } + } + }; + + _.alert = function() { + if(this.text) this.jQ.empty().html(this.text); + this.clear(); + }; + + _.clear = function() { + this.text = ""; + }; +}); From 6e0a63814a4affb64f42f6dd1915efb084df822c Mon Sep 17 00:00:00 2001 From: Stephen Clower Date: Wed, 24 Feb 2016 15:51:32 -0500 Subject: [PATCH 002/393] Plugging an instance of Aria into the main controller. --- src/controller.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/controller.js b/src/controller.js index 73c8a4bba..1e2d10e26 100644 --- a/src/controller.js +++ b/src/controller.js @@ -17,6 +17,8 @@ var Controller = P(function(_) { root.controller = this; + this.aria = Aria(); + this.cursor = root.cursor = Cursor(root, options); // TODO: stop depending on root.cursor, and rm it }; From 2f65f8ee3ec529c9550bd2de80e4d3af03ac67a2 Mon Sep 17 00:00:00 2001 From: Stephen Clower Date: Wed, 24 Feb 2016 16:05:13 -0500 Subject: [PATCH 003/393] Adding CSS for mq-aria-alert class. --- src/css/mixins/display.less | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/css/mixins/display.less b/src/css/mixins/display.less index 1b08a3afe..8ce390e9a 100644 --- a/src/css/mixins/display.less +++ b/src/css/mixins/display.less @@ -2,3 +2,14 @@ display: -moz-inline-box; display: inline-block; } + +// ARIA alert styling; must technically be visible for browsers to fire needed events (except IE). Common technique is to show them offscreen so visual users aren't impacted. +.mq-aria-alert { + position:absolute; + left:-1000px; + top:-1000px; + width:1px; + height:1px; + text-align: left; + overflow:hidden; +} From b5ba5424fd2fb9dc122e79a8cd330d9bc52ec9fe Mon Sep 17 00:00:00 2001 From: Stephen Clower Date: Mon, 29 Feb 2016 14:13:34 -0500 Subject: [PATCH 004/393] Simplify the steps needed to generate an alert. Queuing still possible in the unlikely event someone needs to prepend text. --- src/services/aria.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/services/aria.js b/src/services/aria.js index b32fbc41d..e874ef5ec 100644 --- a/src/services/aria.js +++ b/src/services/aria.js @@ -34,7 +34,8 @@ var Aria = P(function(_) { } }; - _.alert = function() { + _.alert = function(t) { + if(t!=="") this.queue(t, true); if(this.text) this.jQ.empty().html(this.text); this.clear(); }; From b5f06e18c7468701e5fcd2a61825ffd1a21b1780 Mon Sep 17 00:00:00 2001 From: Stephen Clower Date: Mon, 29 Feb 2016 15:29:40 -0500 Subject: [PATCH 005/393] We only ever need one instance of the ARIA alert object, and it needs to be easily accessible from all modules. --- src/controller.js | 1 - src/services/aria.js | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/controller.js b/src/controller.js index 1e2d10e26..d20ee15ee 100644 --- a/src/controller.js +++ b/src/controller.js @@ -17,7 +17,6 @@ var Controller = P(function(_) { root.controller = this; - this.aria = Aria(); this.cursor = root.cursor = Cursor(root, options); // TODO: stop depending on root.cursor, and rm it diff --git a/src/services/aria.js b/src/services/aria.js index e874ef5ec..cf82661ba 100644 --- a/src/services/aria.js +++ b/src/services/aria.js @@ -44,3 +44,6 @@ var Aria = P(function(_) { this.text = ""; }; }); + +// We only ever need one instance of the ARIA alert object, and it needs to be easily accessible from all modules. +var aria = Aria(); From b2425c75f43d6321c8c0c083bb911820ab9da78d Mon Sep 17 00:00:00 2001 From: Stephen Clower Date: Tue, 1 Mar 2016 06:18:58 -0500 Subject: [PATCH 006/393] Fixing a few comments. --- src/services/keystroke.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/keystroke.js b/src/services/keystroke.js index f3ec668cc..c9bf089c0 100644 --- a/src/services/keystroke.js +++ b/src/services/keystroke.js @@ -148,6 +148,7 @@ Node.open(function(_) { Controller.open(function(_) { this.onNotify(function(e) { + aria.alert(e); if (e === 'move' || e === 'upDown') this.show().clearSelection(); }); _.escapeDir = function(dir, key, e) { From 2899a62f6d16ea32793f20275ca9b390c44d81b5 Mon Sep 17 00:00:00 2001 From: Stephen Clower Date: Tue, 1 Mar 2016 15:48:36 -0500 Subject: [PATCH 007/393] Implement some basic speech capabilities in MQ. Cursor movement sometimes says something. Definitely needs more work. Currently triggering alerts in both keystroke handler and in cursor singleton. Likely need to decide on one or the other eventually. Speech is also massaged to be more verbally intelligible, e.g. square root instead of sqrt. Also very preliminary. Finally fixed some incorrect comments in keystroke handler. --- src/cursor.js | 3 +++ src/services/aria.js | 37 ++++++++++++++++++++++++++++++++++--- src/services/keystroke.js | 9 +++++++-- 3 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/cursor.js b/src/cursor.js index b08b2f6ce..cd9b09e2f 100644 --- a/src/cursor.js +++ b/src/cursor.js @@ -57,6 +57,7 @@ var Cursor = P(Point, function(_) { // by contract, .blur() is called after all has been said and done // and the cursor has actually been moved if (oldParent !== parent && oldParent.blur) oldParent.blur(); + aria.queue(withDir, true); }; _.insDirOf = function(dir, el) { prayDirection(dir); @@ -97,6 +98,8 @@ var Cursor = P(Point, function(_) { var pageX = self.offset().left; to.seek(pageX, self); } + if(this[L]) aria.queue(this[L], true); + else if(this[R]) aria.queue(this[R], true); }; _.offset = function() { //in Opera 11.62, .getBoundingClientRect() and hence jQuery::offset() diff --git a/src/services/aria.js b/src/services/aria.js index e874ef5ec..67d6698df 100644 --- a/src/services/aria.js +++ b/src/services/aria.js @@ -19,10 +19,38 @@ var Aria = P(function(_) { 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); this.text = ""; + this.repDict = { + "+": " plus ", + "-": " minus ", + "*": " times ", + "/": " over ", + "^": " exponent ", + "=": " equals ", + "(": " left paren ", + ")": " right paren ", + "frac": "fraction", + "sqrt": "square root" + }; }; - _.queue = function(t, shouldAppend) { - var spaceChar = " "; + _.massageText = function(t) { + for(var key in this.repDict) { + if (this.repDict.hasOwnProperty(key)) t = t.replace(key, this.repDict[key]); + } + return t; + }; + + + _.queue = function(item, shouldAppend) { + var t = "", spaceChar = " "; + if(typeof(item) === 'object' ) { + if(item.text) t = item.text(); + else if(item.ctrlSeq) t = item.ctrlSeq; + else if(item.ch) t = item.ch; + t = this.massageText(t); + } + else t = item; + if(this.text === "" || t === "") spaceChar = ""; if(t) { if (shouldAppend) { @@ -35,7 +63,7 @@ var Aria = P(function(_) { }; _.alert = function(t) { - if(t!=="") this.queue(t, true); + if(t) this.queue(t, true); if(this.text) this.jQ.empty().html(this.text); this.clear(); }; @@ -44,3 +72,6 @@ var Aria = P(function(_) { this.text = ""; }; }); + +// We only ever need one instance of the ARIA alert object, and it needs to be easily accessible from all modules. +var aria = Aria(); diff --git a/src/services/keystroke.js b/src/services/keystroke.js index f3ec668cc..002478e85 100644 --- a/src/services/keystroke.js +++ b/src/services/keystroke.js @@ -65,7 +65,7 @@ Node.open(function(_) { ctrlr.notify('move').cursor.insAtLeftEnd(cursor.parent); break; - // Ctrl-Home -> move to the start of the current block. + // Ctrl-Home -> move to the start of the root block. case 'Ctrl-Home': ctrlr.notify('move').cursor.insAtLeftEnd(ctrlr.root); break; @@ -77,7 +77,7 @@ Node.open(function(_) { } break; - // Ctrl-Shift-Home -> move to the start of the root block. + // Ctrl-Shift-Home -> select to the start of the root block. case 'Ctrl-Shift-Home': while (cursor[L] || cursor.parent !== ctrlr.root) { ctrlr.selectLeft(); @@ -132,6 +132,7 @@ Node.open(function(_) { default: return; } + aria.alert(); e.preventDefault(); ctrlr.scrollHoriz(); }; @@ -161,6 +162,8 @@ Controller.open(function(_) { // default browser action if so) if (cursor.parent === this.root) return; + if (dir === L) aria.queue("escape left", true); + else aria.queue("escape right", true); cursor.parent.moveOutOf(dir, cursor); return this.notify('move'); }; @@ -224,6 +227,7 @@ Controller.open(function(_) { _.deleteDir = function(dir) { prayDirection(dir); var cursor = this.cursor; + if(cursor[dir]) aria.queue(cursor[dir], true); var hadSelection = cursor.selection; this.notify('edit'); // deletes selection if present @@ -277,6 +281,7 @@ Controller.open(function(_) { cursor.clearSelection(); cursor.select() || cursor.show(); + if(cursor.selection) aria.queue(cursor.selection.join('latex') + " selected", true); }; _.selectLeft = function() { return this.selectDir(L); }; _.selectRight = function() { return this.selectDir(R); }; From c91843602b3b4b81bc48c5ae0d6106405011a335 Mon Sep 17 00:00:00 2001 From: Stephen Clower Date: Tue, 1 Mar 2016 15:53:36 -0500 Subject: [PATCH 008/393] Fix a merge error from previous commit. --- src/services/keystroke.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/keystroke.js b/src/services/keystroke.js index 9195da853..002478e85 100644 --- a/src/services/keystroke.js +++ b/src/services/keystroke.js @@ -149,7 +149,6 @@ Node.open(function(_) { Controller.open(function(_) { this.onNotify(function(e) { - aria.alert(e); if (e === 'move' || e === 'upDown') this.show().clearSelection(); }); _.escapeDir = function(dir, key, e) { From 22542611bb43a1d49c85b29f21a9bc43f8fa752b Mon Sep 17 00:00:00 2001 From: Stephen Clower Date: Wed, 2 Mar 2016 06:39:34 -0500 Subject: [PATCH 009/393] Adding some tips for building in Windows. --- BUILDING | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/BUILDING b/BUILDING index 33fc2f205..b60c43354 100644 --- a/BUILDING +++ b/BUILDING @@ -15,5 +15,9 @@ re-make, and also serve the demo, the unit tests, and the visual tests. unit tests -> http://localhost:9292/test/unit.html visual tests -> http://localhost:9292/test/visual.html +If building on Windows: + 1. Install GNU Make from http://gnuwin32.sourceforge.net/packages/make.htm. Do not use make derivitives from mSYS or MinGW. Ensure the location of make.exe is added to your PATH environment variable. + 2. Grab the latest Git for Windows from https://git-scm.com/download/win. When installing, add Git and its optional shell tools to your PATH environment variable (these questions are asked during setup). + If any of this does not work, please let us know! We want to make hacking on mathquill as easy as possible. From 9c00b40f7dd1f4b9d882e4d879edca3792737fd6 Mon Sep 17 00:00:00 2001 From: Han Seoul-Oh Date: Thu, 3 Mar 2016 20:51:02 -0800 Subject: [PATCH 010/393] Further tweak comments in keystroke handler #comments --- src/services/keystroke.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/keystroke.js b/src/services/keystroke.js index 002478e85..261f81996 100644 --- a/src/services/keystroke.js +++ b/src/services/keystroke.js @@ -53,19 +53,19 @@ Node.open(function(_) { } break; - // Ctrl-Shift-End -> select to the end of the root block. + // Ctrl-Shift-End -> select all the way to the end of the root block. case 'Ctrl-Shift-End': while (cursor[R] || cursor.parent !== ctrlr.root) { ctrlr.selectRight(); } break; - // Home -> move to the start of the root block or the current block. + // Home -> move to the start of the current block. case 'Home': ctrlr.notify('move').cursor.insAtLeftEnd(cursor.parent); break; - // Ctrl-Home -> move to the start of the root block. + // Ctrl-Home -> move all the way to the start of the root block. case 'Ctrl-Home': ctrlr.notify('move').cursor.insAtLeftEnd(ctrlr.root); break; @@ -77,7 +77,7 @@ Node.open(function(_) { } break; - // Ctrl-Shift-Home -> select to the start of the root block. + // Ctrl-Shift-Home -> select all the way to the start of the root block. case 'Ctrl-Shift-Home': while (cursor[L] || cursor.parent !== ctrlr.root) { ctrlr.selectLeft(); From 091de223160bbe9956e8506df23287d7c3406f5f Mon Sep 17 00:00:00 2001 From: Han Seoul-Oh Date: Thu, 3 Mar 2016 20:51:38 -0800 Subject: [PATCH 011/393] Add some missing whitespace #style #cosmetic --- src/controller.js | 1 - src/css/mixins/display.less | 12 ++++++------ src/cursor.js | 4 ++-- src/services/aria.js | 2 +- src/services/keystroke.js | 4 ++-- 5 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/controller.js b/src/controller.js index d20ee15ee..73c8a4bba 100644 --- a/src/controller.js +++ b/src/controller.js @@ -17,7 +17,6 @@ var Controller = P(function(_) { root.controller = this; - this.cursor = root.cursor = Cursor(root, options); // TODO: stop depending on root.cursor, and rm it }; diff --git a/src/css/mixins/display.less b/src/css/mixins/display.less index 8ce390e9a..ec58d1836 100644 --- a/src/css/mixins/display.less +++ b/src/css/mixins/display.less @@ -5,11 +5,11 @@ // ARIA alert styling; must technically be visible for browsers to fire needed events (except IE). Common technique is to show them offscreen so visual users aren't impacted. .mq-aria-alert { - position:absolute; - left:-1000px; - top:-1000px; - width:1px; - height:1px; + position: absolute; + left: -1000px; + top: -1000px; + width: 1px; + height: 1px; text-align: left; - overflow:hidden; + overflow: hidden; } diff --git a/src/cursor.js b/src/cursor.js index cd9b09e2f..e4483e8fa 100644 --- a/src/cursor.js +++ b/src/cursor.js @@ -98,8 +98,8 @@ var Cursor = P(Point, function(_) { var pageX = self.offset().left; to.seek(pageX, self); } - if(this[L]) aria.queue(this[L], true); - else if(this[R]) aria.queue(this[R], true); + if (this[L]) aria.queue(this[L], true); + else if (this[R]) aria.queue(this[R], true); }; _.offset = function() { //in Opera 11.62, .getBoundingClientRect() and hence jQuery::offset() diff --git a/src/services/aria.js b/src/services/aria.js index 67d6698df..d0299f499 100644 --- a/src/services/aria.js +++ b/src/services/aria.js @@ -34,7 +34,7 @@ var Aria = P(function(_) { }; _.massageText = function(t) { - for(var key in this.repDict) { + for (var key in this.repDict) { if (this.repDict.hasOwnProperty(key)) t = t.replace(key, this.repDict[key]); } return t; diff --git a/src/services/keystroke.js b/src/services/keystroke.js index 261f81996..4a1f414b0 100644 --- a/src/services/keystroke.js +++ b/src/services/keystroke.js @@ -227,7 +227,7 @@ Controller.open(function(_) { _.deleteDir = function(dir) { prayDirection(dir); var cursor = this.cursor; - if(cursor[dir]) aria.queue(cursor[dir], true); + if (cursor[dir]) aria.queue(cursor[dir], true); var hadSelection = cursor.selection; this.notify('edit'); // deletes selection if present @@ -281,7 +281,7 @@ Controller.open(function(_) { cursor.clearSelection(); cursor.select() || cursor.show(); - if(cursor.selection) aria.queue(cursor.selection.join('latex') + " selected", true); + if (cursor.selection) aria.queue(cursor.selection.join('latex') + " selected", true); }; _.selectLeft = function() { return this.selectDir(L); }; _.selectRight = function() { return this.selectDir(R); }; From ab5a2204dc581585315b4799baeb661ae5607741 Mon Sep 17 00:00:00 2001 From: Han Seoul-Oh Date: Thu, 3 Mar 2016 22:45:54 -0800 Subject: [PATCH 012/393] Fix test failures from .text() returning array instead of str This line: https://github.com/sclower/mathquill/commit/2899a62f6d16ea32793f20275ca9b390c44d81b5#diff-077222065028f8818e74c25445ab542eR47 had been assuming, quite reasonably, that .text() on a node returns a string, but apparently Symbol::text() had been returning an array and no one had noticed because in JS, concatenating an array onto a string will stringify the array. --- src/commands/math.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/math.js b/src/commands/math.js index 63341e3c0..321bd6219 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -325,7 +325,7 @@ var Symbol = P(MathCommand, function(_, super_) { }; _.latex = function(){ return this.ctrlSeq; }; - _.text = function(){ return this.textTemplate; }; + _.text = function(){ return this.textTemplate.join(''); }; _.placeCursor = noop; _.isEmpty = function(){ return true; }; }); From 357b50507b7a2c9590d0198322f483e89fd021d6 Mon Sep 17 00:00:00 2001 From: Han Seoul-Oh Date: Thu, 3 Mar 2016 22:58:15 -0800 Subject: [PATCH 013/393] Add very very basic MathSpeak implementation Doesn't deal with nesting at all. In fact, nesting is especially wrong for SupSub, since it says "Baseline" at the end whether you returned to the baseline or not. --- src/commands/math.js | 15 +++++++++++++++ src/commands/math/commands.js | 5 +++++ src/commands/text.js | 1 + src/publicapi.js | 1 + src/services/aria.js | 6 ++++++ test/unit/publicapi.test.js | 5 +++++ 6 files changed, 33 insertions(+) diff --git a/src/commands/math.js b/src/commands/math.js index 321bd6219..068b928c9 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -288,6 +288,14 @@ var MathCommand = P(MathElement, function(_, super_) { return text + child.text() + (cmd.textTemplate[i] || ''); }); }; + _.mathspeakTemplate = []; + _.mathspeak = function() { + 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); + }); + }; }); /** @@ -325,6 +333,7 @@ var Symbol = P(MathCommand, function(_, super_) { }; _.latex = function(){ return this.ctrlSeq; }; + _.mathspeak = _.text = function(){ return this.textTemplate.join(''); }; _.placeCursor = noop; _.isEmpty = function(){ return true; }; @@ -361,6 +370,12 @@ var MathBlock = P(MathElement, function(_, super_) { this.join('text') ; }; + _.mathspeak = function() { + return this.foldChildren([], function(speechArray, cmd) { + speechArray.push(cmd.mathspeak()); + return speechArray; + }).join(' '); + }; _.keystroke = function(key, e, ctrlr) { if (ctrlr.options.spaceBehavesLikeTab diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index bfc2f1a14..80b841ad5 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -290,6 +290,7 @@ LatexCmds._ = P(SupSub, function(_, super_) { + '' ; _.textTemplate = [ '_' ]; + _.mathspeakTemplate = [ 'Subscript', 'Baseline' ]; _.finalizeTree = function() { this.downInto = this.sub = this.ends[L]; this.sub.upOutOf = insLeftOfMeUnlessAtEnd; @@ -307,6 +308,7 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { + '' ; _.textTemplate = [ '^' ]; + _.mathspeakTemplate = [ 'Superscript', 'Baseline' ]; _.finalizeTree = function() { this.upInto = this.sup = this.ends[R]; this.sup.downOutOf = insLeftOfMeUnlessAtEnd; @@ -392,6 +394,7 @@ LatexCmds.fraction = P(MathCommand, function(_, super_) { + '' ; _.textTemplate = ['(', ')/(', ')']; + _.mathspeakTemplate = ['StartFraction', 'Over', 'EndFraction']; _.finalizeTree = function() { this.upInto = this.ends[R].upOutOf = this.ends[L]; this.downInto = this.ends[L].downOutOf = this.ends[R]; @@ -440,6 +443,7 @@ LatexCmds['√'] = P(MathCommand, function(_, super_) { + '' ; _.textTemplate = ['sqrt(', ')']; + _.mathspeakTemplate = ['StartRoot', 'EndRoot']; _.parser = function() { return latexMathParser.optBlock.then(function(optBlock) { return latexMathParser.block.map(function(block) { @@ -720,6 +724,7 @@ LatexCmds.binomial = P(P(MathCommand, DelimsMixin), function(_, super_) { + '' ; _.textTemplate = ['choose(',',',')']; + _.mathspeakTemplate = ['StartBinomial', 'Choose', 'EndBinomial']; }); var Choose = diff --git a/src/commands/text.js b/src/commands/text.js index 42f85d4b4..960bca849 100644 --- a/src/commands/text.js +++ b/src/commands/text.js @@ -72,6 +72,7 @@ var TextBlock = P(Node, function(_, super_) { + '' ); }; + _.mathspeak = function() { return { speech: this.text() }; }; // editability methods: called by the cursor for editing, cursor movements, // and selection of the MathQuill tree, these all take in a direction and diff --git a/src/publicapi.js b/src/publicapi.js index 0c51677e5..861741cd0 100644 --- a/src/publicapi.js +++ b/src/publicapi.js @@ -114,6 +114,7 @@ function getInterface(v) { _.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) { if (arguments.length > 0) { this.__controller.renderLatexMath(latex); diff --git a/src/services/aria.js b/src/services/aria.js index d0299f499..f2f1fa86e 100644 --- a/src/services/aria.js +++ b/src/services/aria.js @@ -75,3 +75,9 @@ var Aria = P(function(_) { // 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(_) { + // 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/test/unit/publicapi.test.js b/test/unit/publicapi.test.js index 9077a1d68..262e28eae 100644 --- a/test/unit/publicapi.test.js +++ b/test/unit/publicapi.test.js @@ -155,6 +155,11 @@ suite('Public API', function() { assert.equal(mq.__controller.cursor[L].ctrlSeq, '0'); assert.equal(mq.__controller.cursor[R], 0); }); + + test('.mathspeak()', function() { + mq.latex('\\frac{d}{dx}\\sqrt{x}'); + assert.equal(mq.mathspeak(), 'StartFraction d Over d x EndFraction StartRoot x EndRoot'); + }); }); test('edit handler interface versioning', function() { From e86a55de8135df06b553c3cd83f3bb486bba51a0 Mon Sep 17 00:00:00 2001 From: Han Seoul-Oh Date: Thu, 3 Mar 2016 23:57:25 -0800 Subject: [PATCH 014/393] Fix MathSpeak for arithmetic symbols and parens/brackets/braces All the things in repDict in services/aria.js are now covered Also, some pretty nontrivial things are now spoken according to the MathSpeak "spec" (under specific verbosity and explicitness settings), see the "Example 13" test case --- src/commands/math.js | 13 +++++++------ src/commands/math/basicSymbols.js | 12 ++++++------ src/commands/math/commands.js | 26 ++++++++++++++++++++++---- test/unit/publicapi.test.js | 13 +++++++++++++ 4 files changed, 48 insertions(+), 16 deletions(-) diff --git a/src/commands/math.js b/src/commands/math.js index 068b928c9..cac0e939b 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -302,9 +302,10 @@ var MathCommand = P(MathElement, function(_, super_) { * Lightweight command without blocks or children. */ var Symbol = P(MathCommand, function(_, super_) { - _.init = function(ctrlSeq, html, text) { + _.init = function(ctrlSeq, html, text, mathspeak) { if (!text) text = ctrlSeq && ctrlSeq.length > 1 ? ctrlSeq.slice(1) : ctrlSeq; + this.mathspeakName = mathspeak || text; super_.init.call(this, ctrlSeq, html, [ text ]); }; @@ -333,20 +334,20 @@ var Symbol = P(MathCommand, function(_, super_) { }; _.latex = function(){ return this.ctrlSeq; }; - _.mathspeak = _.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) { - super_.init.call(this, ch, ''+(html || ch)+''); + _.init = function(ch, html, mathspeak) { + super_.init.call(this, ch, ''+(html || ch)+'', undefined, mathspeak); }; }); var BinaryOperator = P(Symbol, function(_, super_) { - _.init = function(ctrlSeq, html, text) { + _.init = function(ctrlSeq, html, text, mathspeak) { super_.init.call(this, - ctrlSeq, ''+html+'', text + ctrlSeq, ''+html+'', text, mathspeak ); }; }); diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index 361009fc2..7eab2e6bf 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -406,16 +406,16 @@ var PlusMinus = P(BinaryOperator, function(_) { }; }); -LatexCmds['+'] = bind(PlusMinus, '+', '+'); +LatexCmds['+'] = bind(PlusMinus, '+', '+', 'plus'); //yes, these are different dashes, I think one is an en dash and the other is a hyphen -LatexCmds['–'] = LatexCmds['-'] = bind(PlusMinus, '-', '−'); +LatexCmds['–'] = LatexCmds['-'] = bind(PlusMinus, '-', '−', 'minus'); LatexCmds['±'] = LatexCmds.pm = LatexCmds.plusmn = LatexCmds.plusminus = - bind(PlusMinus,'\\pm ','±'); + bind(PlusMinus,'\\pm ','±', 'plus-or-minus'); LatexCmds.mp = LatexCmds.mnplus = LatexCmds.minusplus = - bind(PlusMinus,'\\mp ','∓'); + bind(PlusMinus,'\\mp ','∓', 'minus-or-plus'); CharCmds['*'] = LatexCmds.sdot = LatexCmds.cdot = - bind(BinaryOperator, '\\cdot ', '·'); + bind(BinaryOperator, '\\cdot ', '·', '*', 'times'); //semantically should be ⋅, but · looks better var Inequality = P(BinaryOperator, function(_, super_) { @@ -455,7 +455,7 @@ LatexCmds['≥'] = LatexCmds.ge = LatexCmds.geq = bind(Inequality, greater, fals var Equality = P(BinaryOperator, function(_, super_) { _.init = function() { - super_.init.call(this, '=', '='); + super_.init.call(this, '=', '=', '=', 'equals'); }; _.createLeftOf = function(cursor) { if (cursor[L] instanceof Inequality && cursor[L].strict) { diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 80b841ad5..ab79ea8ee 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -529,6 +529,16 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) { _.latex = function() { return '\\left'+this.sides[L].ctrlSeq+this.ends[L].latex()+'\\right'+this.sides[R].ctrlSeq; }; + _.mathspeak = function() { + var open = this.sides[L].ch, close = this.sides[R].ch; + if (open === '|' && close === '|') { + this.mathspeakTemplate = ['StartAbsoluteValue', 'EndAbsoluteValue']; + } + else { + this.mathspeakTemplate = ['left-' + BRACKET_NAMES[open], 'right-' + BRACKET_NAMES[close]]; + } + return super_.mathspeak.call(this); + }; _.oppBrack = function(opts, node, expectedSide) { // return node iff it's a 1-sided bracket of expected side (if any, may be // undefined), and of opposite side from me if I'm not a pipe @@ -663,18 +673,26 @@ var OPP_BRACKS = { '|': '|' }; -function bindCharBracketPair(open, ctrlSeq) { +var BRACKET_NAMES = { + '⟨': 'angle-bracket', + '⟩': 'angle-bracket', + '|': '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; } -bindCharBracketPair('('); -bindCharBracketPair('['); -bindCharBracketPair('{', '\\{'); +bindCharBracketPair('(', null, 'parenthesis'); +bindCharBracketPair('[', null, 'bracket'); +bindCharBracketPair('{', '\\{', 'brace'); LatexCmds.langle = bind(Bracket, L, '⟨', '⟩', '\\langle ', '\\rangle '); LatexCmds.rangle = bind(Bracket, R, '⟨', '⟩', '\\langle ', '\\rangle '); CharCmds['|'] = bind(Bracket, L, '|', '|', '|', '|'); + LatexCmds.left = P(MathCommand, function(_) { _.parser = function() { var regex = Parser.regex; diff --git a/test/unit/publicapi.test.js b/test/unit/publicapi.test.js index 262e28eae..432c5e1a3 100644 --- a/test/unit/publicapi.test.js +++ b/test/unit/publicapi.test.js @@ -159,6 +159,19 @@ suite('Public API', function() { test('.mathspeak()', function() { mq.latex('\\frac{d}{dx}\\sqrt{x}'); assert.equal(mq.mathspeak(), 'StartFraction d Over d x EndFraction StartRoot x EndRoot'); + + mq.latex('1+2-3\\cdot\\frac{5}{6^7}=\\left(8+9\\right)'); + assert.equal(mq.mathspeak(), '1 plus 2 minus 3 times StartFraction 5 Over 6 Superscript 7 Baseline EndFraction equals left-parenthesis 8 plus 9 right-parenthesis'); + + // Example 13 from http://www.gh-mathspeak.com/examples/quick-tutorial/index.php?verbosity=v&explicitness=2&interp=0 + mq.latex('d=\\sqrt{ \\left( x_2 - x_1 \\right)^2 - \\left( y_2 - y_1 \\right)^2 }'); + assert.equal(mq.mathspeak(), 'd equals StartRoot left-parenthesis x Subscript 2 Baseline minus x Subscript 1 Baseline right-parenthesis Superscript 2 Baseline minus left-parenthesis y Subscript 2 Baseline minus y Subscript 1 Baseline right-parenthesis Superscript 2 Baseline EndRoot'); + + mq.latex('').typedText('\\langle').keystroke('Spacebar').typedText('u,v'); // .latex() doesn't work yet for angle brackets :( + assert.equal(mq.mathspeak(), 'left-angle-bracket u , v right-angle-bracket'); + + mq.latex('\\left| x \\right| + \\left( y \\right|'); + assert.equal(mq.mathspeak(), 'StartAbsoluteValue x EndAbsoluteValue plus left-parenthesis y right-pipe'); }); }); From 1f6282cb5c4083ea0370c88a6b97c86773937056 Mon Sep 17 00:00:00 2001 From: Han Seoul-Oh Date: Fri, 4 Mar 2016 00:11:09 -0800 Subject: [PATCH 015/393] Use node.mathspeak() instead of massageText and repDict This way, the way to pronounce something in mathspeak is the responsibility of the node/command in question, rather than being centralized; just like all the other services. --- src/services/aria.js | 27 +-------------------------- 1 file changed, 1 insertion(+), 26 deletions(-) diff --git a/src/services/aria.js b/src/services/aria.js index f2f1fa86e..a2a11e7b5 100644 --- a/src/services/aria.js +++ b/src/services/aria.js @@ -19,36 +19,11 @@ var Aria = P(function(_) { 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); this.text = ""; - this.repDict = { - "+": " plus ", - "-": " minus ", - "*": " times ", - "/": " over ", - "^": " exponent ", - "=": " equals ", - "(": " left paren ", - ")": " right paren ", - "frac": "fraction", - "sqrt": "square root" - }; }; - _.massageText = function(t) { - for (var key in this.repDict) { - if (this.repDict.hasOwnProperty(key)) t = t.replace(key, this.repDict[key]); - } - return t; - }; - - _.queue = function(item, shouldAppend) { var t = "", spaceChar = " "; - if(typeof(item) === 'object' ) { - if(item.text) t = item.text(); - else if(item.ctrlSeq) t = item.ctrlSeq; - else if(item.ch) t = item.ch; - t = this.massageText(t); - } + if (item instanceof Node) t = item.mathspeak(); else t = item; if(this.text === "" || t === "") spaceChar = ""; From cefeeb7f3c9b2b9803ff7f07efda2d16d406c75f Mon Sep 17 00:00:00 2001 From: Han Seoul-Oh Date: Fri, 4 Mar 2016 00:17:35 -0800 Subject: [PATCH 016/393] Remove shouldAppend parameter from Aria::queue() Every single call to it passes in true. Even if someday we want to prepend instead of append, it should default to append and passing in true should cause prepend. --- src/cursor.js | 6 +++--- src/services/aria.js | 11 +++-------- src/services/keystroke.js | 8 ++++---- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/cursor.js b/src/cursor.js index e4483e8fa..34ff150d2 100644 --- a/src/cursor.js +++ b/src/cursor.js @@ -57,7 +57,7 @@ var Cursor = P(Point, function(_) { // by contract, .blur() is called after all has been said and done // and the cursor has actually been moved if (oldParent !== parent && oldParent.blur) oldParent.blur(); - aria.queue(withDir, true); + aria.queue(withDir); }; _.insDirOf = function(dir, el) { prayDirection(dir); @@ -98,8 +98,8 @@ var Cursor = P(Point, function(_) { var pageX = self.offset().left; to.seek(pageX, self); } - if (this[L]) aria.queue(this[L], true); - else if (this[R]) aria.queue(this[R], true); + if (this[L]) aria.queue(this[L]); + else if (this[R]) aria.queue(this[R]); }; _.offset = function() { //in Opera 11.62, .getBoundingClientRect() and hence jQuery::offset() diff --git a/src/services/aria.js b/src/services/aria.js index a2a11e7b5..b965b14f4 100644 --- a/src/services/aria.js +++ b/src/services/aria.js @@ -21,24 +21,19 @@ var Aria = P(function(_) { this.text = ""; }; - _.queue = function(item, shouldAppend) { + _.queue = function(item) { var t = "", spaceChar = " "; if (item instanceof Node) t = item.mathspeak(); else t = item; if(this.text === "" || t === "") spaceChar = ""; if(t) { - if (shouldAppend) { - this.text = this.text + spaceChar + t; - } - else { - this.text = t + spaceChar + this.text; - } + this.text = this.text + spaceChar + t; } }; _.alert = function(t) { - if(t) this.queue(t, true); + if(t) this.queue(t); if(this.text) this.jQ.empty().html(this.text); this.clear(); }; diff --git a/src/services/keystroke.js b/src/services/keystroke.js index 4a1f414b0..a128e65b7 100644 --- a/src/services/keystroke.js +++ b/src/services/keystroke.js @@ -162,8 +162,8 @@ Controller.open(function(_) { // default browser action if so) if (cursor.parent === this.root) return; - if (dir === L) aria.queue("escape left", true); - else aria.queue("escape right", true); + if (dir === L) aria.queue("escape left"); + else aria.queue("escape right"); cursor.parent.moveOutOf(dir, cursor); return this.notify('move'); }; @@ -227,7 +227,7 @@ Controller.open(function(_) { _.deleteDir = function(dir) { prayDirection(dir); var cursor = this.cursor; - if (cursor[dir]) aria.queue(cursor[dir], true); + if (cursor[dir]) aria.queue(cursor[dir]); var hadSelection = cursor.selection; this.notify('edit'); // deletes selection if present @@ -281,7 +281,7 @@ Controller.open(function(_) { cursor.clearSelection(); cursor.select() || cursor.show(); - if (cursor.selection) aria.queue(cursor.selection.join('latex') + " selected", true); + if (cursor.selection) aria.queue(cursor.selection.join('latex') + " selected"); }; _.selectLeft = function() { return this.selectDir(L); }; _.selectRight = function() { return this.selectDir(R); }; From 6d5104b59b6ae3b8b5b0902d80b2434422e84c99 Mon Sep 17 00:00:00 2001 From: Han Seoul-Oh Date: Fri, 4 Mar 2016 00:28:24 -0800 Subject: [PATCH 017/393] Aria() queues up array rather than string That way can simply `.join(' ')`, rather than doing this tricky business with `spaceChar`. --- src/services/aria.js | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/services/aria.js b/src/services/aria.js index b965b14f4..a2416ad0f 100644 --- a/src/services/aria.js +++ b/src/services/aria.js @@ -18,28 +18,22 @@ var Aria = P(function(_) { // 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); - this.text = ""; + this.items = []; }; _.queue = function(item) { - var t = "", spaceChar = " "; - if (item instanceof Node) t = item.mathspeak(); - else t = item; - - if(this.text === "" || t === "") spaceChar = ""; - if(t) { - this.text = this.text + spaceChar + t; - } + if (item instanceof Node) item = item.mathspeak(); + this.items.push(item); }; _.alert = function(t) { if(t) this.queue(t); - if(this.text) this.jQ.empty().html(this.text); + if(this.items.length) this.jQ.empty().html(this.items.join(' ')); this.clear(); }; _.clear = function() { - this.text = ""; + this.items.length = 0; }; }); From eff0e56ec22b9632efaded216098149fb87d42ff Mon Sep 17 00:00:00 2001 From: Han Seoul-Oh Date: Fri, 4 Mar 2016 00:28:37 -0800 Subject: [PATCH 018/393] Fix 'if (' spacing #whitespace #style #cosmetic --- src/services/aria.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/aria.js b/src/services/aria.js index a2416ad0f..e3a12501d 100644 --- a/src/services/aria.js +++ b/src/services/aria.js @@ -27,8 +27,8 @@ var Aria = P(function(_) { }; _.alert = function(t) { - if(t) this.queue(t); - if(this.items.length) this.jQ.empty().html(this.items.join(' ')); + if (t) this.queue(t); + if (this.items.length) this.jQ.empty().html(this.items.join(' ')); this.clear(); }; From b23cfc8ca1a5bef07abb5c48ac7c84e6c228ac10 Mon Sep 17 00:00:00 2001 From: Han Seoul-Oh Date: Fri, 4 Mar 2016 00:34:15 -0800 Subject: [PATCH 019/393] Enable chaining of Aria methods --- src/services/aria.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/aria.js b/src/services/aria.js index e3a12501d..dc1a10bd5 100644 --- a/src/services/aria.js +++ b/src/services/aria.js @@ -24,16 +24,18 @@ var Aria = P(function(_) { _.queue = function(item) { if (item instanceof Node) item = item.mathspeak(); this.items.push(item); + return this; }; _.alert = function(t) { if (t) this.queue(t); if (this.items.length) this.jQ.empty().html(this.items.join(' ')); - this.clear(); + return this.clear(); }; _.clear = function() { this.items.length = 0; + return this; }; }); From 1844a4e182d435d1df54fadf2dfaf564d283b927 Mon Sep 17 00:00:00 2001 From: Han Seoul-Oh Date: Fri, 4 Mar 2016 01:04:32 -0800 Subject: [PATCH 020/393] Add aria-hidden=true to textarea To disable all the annoying "beginning of text" and "end of text" announcements that VoiceOver speaks. @sclower does that happen with the screen readers you've tried? Does this fix it? --- src/services/textarea.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/textarea.js b/src/services/textarea.js index dd2d2de65..78d3611d5 100644 --- a/src/services/textarea.js +++ b/src/services/textarea.js @@ -6,7 +6,7 @@ Controller.open(function(_) { Options.p.substituteTextarea = function() { return $(' + + + + + + + + From 91a917d892363b0c17d6f42474a24267a5169354 Mon Sep 17 00:00:00 2001 From: skondam Date: Fri, 23 Feb 2018 18:45:37 -0600 Subject: [PATCH 240/393] support for overarc remove important modifiers --- src/css/math.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/css/math.less b/src/css/math.less index d9cd40829..e31f1fe25 100644 --- a/src/css/math.less +++ b/src/css/math.less @@ -346,8 +346,8 @@ .mq-overarc { border-top: 1px solid black; border-radius: 50% 50% 0 0; - padding: 0.35em 0.25em 0 0.1em !important; - margin-top: 0.1em !important; + padding: 0.35em 0.25em 0 0.1em; + margin-top: 0.1em; } .mq-overarrow { From dbc44a069d9ca683d152c3aac8f6f40beb79318f Mon Sep 17 00:00:00 2001 From: Eli Luberoff Date: Mon, 26 Feb 2018 16:45:20 -0800 Subject: [PATCH 241/393] update less version dependency less version 3.0.0 causes problems with inline javascript, which was breaking the build --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 5bf326333..430aa24be 100644 --- a/package.json +++ b/package.json @@ -13,9 +13,9 @@ "quickstart.html" ], "devDependencies": { - "pjs": ">=3.1.0 <5.0.0", + "less": ">=1.5.1 <3.0.0", "mocha": ">=2.4.1", - "uglify-js": "2.x", - "less": ">=1.5.1" + "pjs": ">=3.1.0 <5.0.0", + "uglify-js": "2.x" } } From 0d0fdc532a42b9e52f0db1ecf8bcb7ddf4f4986f Mon Sep 17 00:00:00 2001 From: Eli Luberoff Date: Mon, 26 Feb 2018 16:52:18 -0800 Subject: [PATCH 242/393] add a few more minus signs --- src/commands/math/basicSymbols.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index 7649e9463..97a9b5ccf 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -602,8 +602,8 @@ var PlusMinus = P(BinaryOperator, function(_) { }); LatexCmds['+'] = bind(PlusMinus, '+', '+', 'plus'); -//yes, these are different dashes, I think one is an en dash and the other is a hyphen -LatexCmds['–'] = LatexCmds['-'] = bind(PlusMinus, '-', '−', 'minus'); +//yes, these are different dashes, en-dash, em-dash, unicode minus, actual dash +LatexCmds['−'] = LatexCmds['—'] = LatexCmds['–'] = LatexCmds['-'] = bind(PlusMinus, '-', '−', 'minus'); LatexCmds['±'] = LatexCmds.pm = LatexCmds.plusmn = LatexCmds.plusminus = bind(PlusMinus,'\\pm ','±', 'plus-or-minus'); LatexCmds.mp = LatexCmds.mnplus = LatexCmds.minusplus = From eaa9becdb14d0ff733f49ace1670795662564796 Mon Sep 17 00:00:00 2001 From: Eli Luberoff Date: Mon, 26 Feb 2018 16:58:37 -0800 Subject: [PATCH 243/393] fix broken unit tests we aren't gauranteeing that controller.options is an object --- src/services/saneKeyboardEvents.util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/saneKeyboardEvents.util.js b/src/services/saneKeyboardEvents.util.js index fd889a0fe..03826a79d 100644 --- a/src/services/saneKeyboardEvents.util.js +++ b/src/services/saneKeyboardEvents.util.js @@ -275,7 +275,7 @@ var saneKeyboardEvents = (function() { // -*- attach event handlers -*- // - if (controller.options.disableCopyPaste) { + if (controller.options && controller.options.disableCopyPaste) { target.bind({ keydown: onKeydown, keypress: onKeypress, From 7057f0141fbffe4a698f06101f4f4ce5833c8d15 Mon Sep 17 00:00:00 2001 From: Eli Luberoff Date: Mon, 26 Feb 2018 16:59:47 -0800 Subject: [PATCH 244/393] add tests that various minus signs are mutated --- test/unit/typing.test.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index a8189f104..f203436e0 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -34,6 +34,15 @@ suite('typing with auto-replaces', function() { }); }); + suite('EquivalentMinus', function() { + test('different minus symbols', function() { + //these 4 are all different characters (!!) + mq.typedText('−—–-'); + //these 4 are all the same character + assertLatex('----'); + }); + }); + suite('LatexCommandInput', function() { test('basic', function() { mq.typedText('\\sqrt-x'); From 968550da40326e3d27f88e0377b8a4bfcd6daaa7 Mon Sep 17 00:00:00 2001 From: Chris Lusto Date: Tue, 27 Feb 2018 18:20:43 -0500 Subject: [PATCH 245/393] Add defensive checks for property lookup on parent-parent nodes We were trying to index into, e.g., `parent.parent.ends[L]`, but it's possible that a parent is actually equal to `0`, in which case there is no `ends` property and the lookup throws. Reported by a partner, but we're also seeing this occasionally on WWW. --- src/services/keystroke.js | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/services/keystroke.js b/src/services/keystroke.js index 5bc13db89..fa59223b5 100644 --- a/src/services/keystroke.js +++ b/src/services/keystroke.js @@ -135,33 +135,49 @@ 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); + if (cursor.parent.parent && cursor.parent.parent instanceof Node) aria.queue(cursor.parent.parent); else 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); + if (cursor.parent && cursor.parent instanceof Node) aria.queue(cursor.parent); else aria.queue('block is empty'); break; case 'Ctrl-Alt-Left': // speak left-adjacent block - if(cursor.parent.parent.ends[L] && cursor.parent.parent.ends[L] instanceof Node) aria.queue(cursor.parent.parent.ends[L]); - else aria.queue('nothing to the left'); + if ( + cursor.parent.parent && + cursor.parent.parent.ends && + cursor.parent.parent.ends[L] && + cursor.parent.parent.ends[L] instanceof Node + ) { + aria.queue(cursor.parent.parent.ends[L]); + } else { + aria.queue('nothing to the left'); + } break; case 'Ctrl-Alt-Right': // speak right-adjacent block - if(cursor.parent.parent.ends[R] && cursor.parent.parent.ends[R] instanceof Node) aria.queue(cursor.parent.parent.ends[R]); - else aria.queue('nothing to the right'); + if ( + cursor.parent.parent && + cursor.parent.parent.ends && + cursor.parent.parent.ends[R] && + cursor.parent.parent.ends[R] instanceof Node + ) { + aria.queue(cursor.parent.parent.ends[R]); + } else { + 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'); + if (cursor.selection) aria.queue(cursor.selection.join('mathspeak', ' ').trim() + ' selected'); else 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); + if (ctrlr.ariaPostLabel.length) aria.queue(ctrlr.ariaPostLabel); else aria.queue('no answer'); break; From a717c0724add084e34e7daa8aa8a66423088f10f Mon Sep 17 00:00:00 2001 From: Jason Merrill Date: Wed, 28 Feb 2018 15:23:20 -0800 Subject: [PATCH 246/393] Check in package-lock. This is a new feature of npm that specifies exact package versions. It gets generated every time you run `npm install` (or `make` or `make basic`), so we need to either check this in or ignore it. Checking it in is helpful because it avoid surprise version differences between users. --- package-lock.json | 898 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 898 insertions(+) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 000000000..01972c97b --- /dev/null +++ b/package-lock.json @@ -0,0 +1,898 @@ +{ + "name": "mathquill", + "version": "0.10.1", + "lockfileVersion": 1, + "requires": true, + "dependencies": { + "ajv": { + "version": "4.11.8", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", + "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", + "dev": true, + "optional": true, + "requires": { + "co": "4.6.0", + "json-stable-stringify": "1.0.1" + } + }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "dev": true, + "requires": { + "kind-of": "3.2.2", + "longest": "1.0.1", + "repeat-string": "1.6.1" + } + }, + "asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=", + "dev": true, + "optional": true + }, + "asn1": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz", + "integrity": "sha1-2sh4dxPJlmhJ/IGAd36+nB3fO4Y=", + "dev": true, + "optional": true + }, + "assert-plus": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz", + "integrity": "sha1-104bh+ev/A24qttwIfP+SBAasjQ=", + "dev": true, + "optional": true + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true, + "optional": true + }, + "aws-sign2": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz", + "integrity": "sha1-FDQt0428yU0OW4fXY81jYSwOeU8=", + "dev": true, + "optional": true + }, + "aws4": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.6.0.tgz", + "integrity": "sha1-g+9cqGCysy5KDe7e6MdxudtXRx4=", + "dev": true, + "optional": true + }, + "balanced-match": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", + "dev": true + }, + "bcrypt-pbkdf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.1.tgz", + "integrity": "sha1-Y7xdy2EzG5K8Bf1SiVPDNGKgb40=", + "dev": true, + "optional": true, + "requires": { + "tweetnacl": "0.14.5" + } + }, + "boom": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz", + "integrity": "sha1-OciRjO/1eZ+D+UkqhI9iWt0Mdm8=", + "dev": true, + "requires": { + "hoek": "2.16.3" + } + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "1.0.0", + "concat-map": "0.0.1" + } + }, + "browser-stdout": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.0.tgz", + "integrity": "sha1-81HTKWnTL6XXpVZxVCY9korjvR8=", + "dev": true + }, + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=", + "dev": true + }, + "caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", + "dev": true, + "optional": true + }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "dev": true, + "requires": { + "align-text": "0.1.4", + "lazy-cache": "1.0.4" + } + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "dev": true, + "requires": { + "center-align": "0.1.3", + "right-align": "0.1.3", + "wordwrap": "0.0.2" + } + }, + "co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", + "dev": true, + "optional": true + }, + "combined-stream": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.6.tgz", + "integrity": "sha1-cj599ugBrFYTETp+RFqbactjKBg=", + "dev": true, + "requires": { + "delayed-stream": "1.0.0" + } + }, + "commander": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz", + "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", + "dev": true, + "optional": true + }, + "cryptiles": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz", + "integrity": "sha1-O9/s3GCBR8HGcgL6KR59ylnqo7g=", + "dev": true, + "optional": true, + "requires": { + "boom": "2.10.1" + } + }, + "dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "debug": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", + "dev": true, + "requires": { + "ms": "2.0.0" + } + }, + "decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", + "dev": true + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true + }, + "diff": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/diff/-/diff-3.3.1.tgz", + "integrity": "sha512-MKPHZDMB0o6yHyDryUOScqZibp914ksXwAMYMTHj6KO8UeKsRYNJD3oNCKjTqZon+V488P7N/HzXF8t7ZR95ww==", + "dev": true + }, + "ecc-jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "integrity": "sha1-D8c6ntXw1Tw4GTOYUj735UN3dQU=", + "dev": true, + "optional": true, + "requires": { + "jsbn": "0.1.1" + } + }, + "errno": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.7.tgz", + "integrity": "sha512-MfrRBDWzIWifgq6tJj60gkAwtLNb6sQPlcFrSOflcP1aFmmruKQ2wRnze/8V6kgyz7H3FF8Npzv78mZ7XLLflg==", + "dev": true, + "optional": true, + "requires": { + "prr": "1.0.1" + } + }, + "escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", + "dev": true + }, + "extend": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.1.tgz", + "integrity": "sha1-p1Xqe8Gt/MWjHOfnYtuq3F5jZEQ=", + "dev": true, + "optional": true + }, + "extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true + }, + "forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true, + "optional": true + }, + "form-data": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.1.4.tgz", + "integrity": "sha1-M8GDrPGTJ27KqYFDpp6Uv+4XUNE=", + "dev": true, + "optional": true, + "requires": { + "asynckit": "0.4.0", + "combined-stream": "1.0.6", + "mime-types": "2.1.18" + } + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", + "dev": true + }, + "getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "glob": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", + "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", + "dev": true, + "requires": { + "fs.realpath": "1.0.0", + "inflight": "1.0.6", + "inherits": "2.0.3", + "minimatch": "3.0.4", + "once": "1.4.0", + "path-is-absolute": "1.0.1" + } + }, + "graceful-fs": { + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", + "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", + "dev": true, + "optional": true + }, + "growl": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/growl/-/growl-1.10.3.tgz", + "integrity": "sha512-hKlsbA5Vu3xsh1Cg3J7jSmX/WaW6A5oBeqzM88oNbCRQFz+zUaXm6yxS4RVytp1scBoJzSYl4YAEOQIt6O8V1Q==", + "dev": true + }, + "har-schema": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-1.0.5.tgz", + "integrity": "sha1-0mMTX0MwfALGAq/I/pWXDAFRNp4=", + "dev": true, + "optional": true + }, + "har-validator": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-4.2.1.tgz", + "integrity": "sha1-M0gdDxu/9gDdID11gSpqX7oALio=", + "dev": true, + "optional": true, + "requires": { + "ajv": "4.11.8", + "har-schema": "1.0.5" + } + }, + "has-flag": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz", + "integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=", + "dev": true + }, + "hawk": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz", + "integrity": "sha1-B4REvXwWQLD+VA0sm3PVlnjo4cQ=", + "dev": true, + "optional": true, + "requires": { + "boom": "2.10.1", + "cryptiles": "2.0.5", + "hoek": "2.16.3", + "sntp": "1.0.9" + } + }, + "he": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/he/-/he-1.1.1.tgz", + "integrity": "sha1-k0EP0hsAlzUVH4howvJx80J+I/0=", + "dev": true + }, + "hoek": { + "version": "2.16.3", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz", + "integrity": "sha1-ILt0A9POo5jpHcRxCo/xuCdKJe0=", + "dev": true + }, + "http-signature": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz", + "integrity": "sha1-33LiZwZs0Kxn+3at+OE0qPvPkb8=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "0.2.0", + "jsprim": "1.4.1", + "sshpk": "1.13.1" + } + }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha1-Cd/Uq50g4p6xw+gLiZA3jfnjy5w=", + "dev": true, + "optional": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "requires": { + "once": "1.4.0", + "wrappy": "1.0.2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", + "dev": true + }, + "is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true, + "optional": true + }, + "isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true, + "optional": true + }, + "jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "dev": true, + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", + "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", + "dev": true, + "optional": true + }, + "json-stable-stringify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", + "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", + "dev": true, + "optional": true, + "requires": { + "jsonify": "0.0.0" + } + }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true, + "optional": true + }, + "jsonify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", + "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", + "dev": true, + "optional": true + }, + "jsprim": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", + "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.2.3", + "verror": "1.10.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "dev": true, + "requires": { + "is-buffer": "1.1.6" + } + }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=", + "dev": true + }, + "less": { + "version": "2.7.3", + "resolved": "https://registry.npmjs.org/less/-/less-2.7.3.tgz", + "integrity": "sha512-KPdIJKWcEAb02TuJtaLrhue0krtRLoRoo7x6BNJIBelO00t/CCdJQUnHW5V34OnHMWzIktSalJxRO+FvytQlCQ==", + "dev": true, + "requires": { + "errno": "0.1.7", + "graceful-fs": "4.1.11", + "image-size": "0.5.5", + "mime": "1.6.0", + "mkdirp": "0.5.1", + "promise": "7.3.1", + "request": "2.81.0", + "source-map": "0.5.7" + } + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true + }, + "mime-db": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", + "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", + "dev": true + }, + "mime-types": { + "version": "2.1.18", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", + "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", + "dev": true, + "requires": { + "mime-db": "1.33.0" + } + }, + "minimatch": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", + "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", + "dev": true, + "requires": { + "brace-expansion": "1.1.11" + } + }, + "minimist": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "dev": true + }, + "mkdirp": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "dev": true, + "requires": { + "minimist": "0.0.8" + } + }, + "mocha": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-5.0.1.tgz", + "integrity": "sha512-SpwyojlnE/WRBNGtvJSNfllfm5PqEDFxcWluSIgLeSBJtXG4DmoX2NNAeEA7rP5kK+79VgtVq8nG6HskaL1ykg==", + "dev": true, + "requires": { + "browser-stdout": "1.3.0", + "commander": "2.11.0", + "debug": "3.1.0", + "diff": "3.3.1", + "escape-string-regexp": "1.0.5", + "glob": "7.1.2", + "growl": "1.10.3", + "he": "1.1.1", + "mkdirp": "0.5.1", + "supports-color": "4.4.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", + "dev": true + }, + "oauth-sign": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz", + "integrity": "sha1-Rqarfwrq2N6unsBWV4C31O/rnUM=", + "dev": true, + "optional": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "requires": { + "wrappy": "1.0.2" + } + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", + "dev": true + }, + "performance-now": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-0.2.0.tgz", + "integrity": "sha1-M+8wxcd9TqIcWlOGnZG1bY8lVeU=", + "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", + "integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==", + "dev": true, + "optional": true, + "requires": { + "asap": "2.0.6" + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "dev": true, + "optional": true + }, + "punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", + "dev": true, + "optional": true + }, + "qs": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.4.0.tgz", + "integrity": "sha1-E+JtKK1rD/qpExLNO/cI7TUecjM=", + "dev": true, + "optional": true + }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", + "dev": true + }, + "request": { + "version": "2.81.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.81.0.tgz", + "integrity": "sha1-xpKJRqDgbF+Nb4qTM0af/aRimKA=", + "dev": true, + "optional": true, + "requires": { + "aws-sign2": "0.6.0", + "aws4": "1.6.0", + "caseless": "0.12.0", + "combined-stream": "1.0.6", + "extend": "3.0.1", + "forever-agent": "0.6.1", + "form-data": "2.1.4", + "har-validator": "4.2.1", + "hawk": "3.1.3", + "http-signature": "1.1.1", + "is-typedarray": "1.0.0", + "isstream": "0.1.2", + "json-stringify-safe": "5.0.1", + "mime-types": "2.1.18", + "oauth-sign": "0.8.2", + "performance-now": "0.2.0", + "qs": "6.4.0", + "safe-buffer": "5.1.1", + "stringstream": "0.0.5", + "tough-cookie": "2.3.4", + "tunnel-agent": "0.6.0", + "uuid": "3.2.1" + } + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "dev": true, + "requires": { + "align-text": "0.1.4" + } + }, + "safe-buffer": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", + "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", + "dev": true + }, + "sntp": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz", + "integrity": "sha1-ZUEYTMkK7qbG57NeJlkIJEPGYZg=", + "dev": true, + "optional": true, + "requires": { + "hoek": "2.16.3" + } + }, + "source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", + "dev": true + }, + "sshpk": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.13.1.tgz", + "integrity": "sha1-US322mKHFEMW3EwY/hzx2UBzm+M=", + "dev": true, + "optional": true, + "requires": { + "asn1": "0.2.3", + "assert-plus": "1.0.0", + "bcrypt-pbkdf": "1.0.1", + "dashdash": "1.14.1", + "ecc-jsbn": "0.1.1", + "getpass": "0.1.7", + "jsbn": "0.1.1", + "tweetnacl": "0.14.5" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "stringstream": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz", + "integrity": "sha1-TkhM1N5aC7vuGORjB3EKioFiGHg=", + "dev": true, + "optional": true + }, + "supports-color": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.4.0.tgz", + "integrity": "sha512-rKC3+DyXWgK0ZLKwmRsrkyHVZAjNkfzeehuFWdGGcqGDTZFH73+RH6S/RDAAxl9GusSjZSUWYLmT9N5pzXFOXQ==", + "dev": true, + "requires": { + "has-flag": "2.0.0" + } + }, + "tough-cookie": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.4.tgz", + "integrity": "sha512-TZ6TTfI5NtZnuyy/Kecv+CnoROnyXn2DN97LontgQpCwsX2XyLYCC0ENhYkehSOwAp8rTQKc/NUIF7BkQ5rKLA==", + "dev": true, + "optional": true, + "requires": { + "punycode": "1.4.1" + } + }, + "tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", + "dev": true, + "optional": true, + "requires": { + "safe-buffer": "5.1.1" + } + }, + "tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", + "dev": true, + "optional": true + }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "dev": true, + "requires": { + "source-map": "0.5.7", + "uglify-to-browserify": "1.0.2", + "yargs": "3.10.0" + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "dev": true, + "optional": true + }, + "uuid": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.2.1.tgz", + "integrity": "sha512-jZnMwlb9Iku/O3smGWvZhauCf6cvvpKi4BKRiliS3cxnI+Gz9j5MEpTz2UFuXiKPJocb7gnsLHwiS05ige5BEA==", + "dev": true, + "optional": true + }, + "verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", + "dev": true, + "optional": true, + "requires": { + "assert-plus": "1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "1.3.0" + }, + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "optional": true + } + } + }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=", + "dev": true + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=", + "dev": true + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "dev": true, + "requires": { + "camelcase": "1.2.1", + "cliui": "2.1.0", + "decamelize": "1.2.0", + "window-size": "0.1.0" + } + } + } +} From 078bd19cfe28de593184560d44a06d6225ac0881 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Thu, 15 Mar 2018 16:14:08 -0400 Subject: [PATCH 247/393] restore legacy substituteKeyboartdEvents option --- src/services/textarea.js | 3 +- test/unit/publicapi.test.js | 41 +++++++++++++++++++++++++ test/visual.html | 60 +++++++++++++++++++++++++++++++++---- 3 files changed, 97 insertions(+), 7 deletions(-) diff --git a/src/services/textarea.js b/src/services/textarea.js index 0c9b45501..44f302d74 100644 --- a/src/services/textarea.js +++ b/src/services/textarea.js @@ -75,10 +75,11 @@ Controller.open(function(_) { var ariaLabel = ctrlr && ctrlr.ariaLabel !== 'MathQuill Input' ? ctrlr.ariaLabel + ': ' : ''; ctrlr.container.attr('aria-label', ariaLabel + root.mathspeak().trim()); }; + Options.p.substituteKeyboardEvents = saneKeyboardEvents; _.editablesTextareaEvents = function() { var ctrlr = this, textarea = ctrlr.textarea, textareaSpan = ctrlr.textareaSpan; - var keyboardEventsShim = saneKeyboardEvents(textarea, this); + var keyboardEventsShim = this.options.substituteKeyboardEvents(textarea, this); this.selectFn = function(text) { keyboardEventsShim.select(text); }; this.container.prepend(textareaSpan); this.focusBlurEvents(); diff --git a/test/unit/publicapi.test.js b/test/unit/publicapi.test.js index 1dae63146..76823be16 100644 --- a/test/unit/publicapi.test.js +++ b/test/unit/publicapi.test.js @@ -840,6 +840,47 @@ suite('Public API', function() { }); }); + suite('substituteKeyboardEvents', function() { + test('can intercept key events', function() { + var mq = MQ.MathField($('').appendTo('#mock')[0], { + substituteKeyboardEvents: function(textarea, handlers) { + return MQ.saneKeyboardEvents(textarea, jQuery.extend({}, handlers, { + keystroke: function(_key, evt) { + key = _key; + return handlers.keystroke.apply(handlers, arguments); + } + })); + } + }); + var key; + + $(mq.el()).find('textarea').trigger({ type: 'keydown', which: '37' }); + assert.equal(key, 'Left'); + }); + test('cut is async', function() { + var mq = MQ.MathField($('').appendTo('#mock')[0], { + substituteKeyboardEvents: function(textarea, handlers) { + return MQ.saneKeyboardEvents(textarea, jQuery.extend({}, handlers, { + cut: function() { + count += 1; + return handlers.cut.apply(handlers, arguments); + } + })); + } + }); + var count = 0; + + $(mq.el()).find('textarea').trigger('cut'); + assert.equal(count, 0); + + $(mq.el()).find('textarea').trigger('input'); + assert.equal(count, 1); + + $(mq.el()).find('textarea').trigger('keyup'); + assert.equal(count, 1); + }); + }); + suite('clickAt', function() { test('inserts at coordinates', function() { // Insert filler so that the page is taller than the window so this test is deterministic diff --git a/test/visual.html b/test/visual.html index fedb172b0..2cb3be29e 100644 --- a/test/visual.html +++ b/test/visual.html @@ -292,6 +292,21 @@

overrideKeystroke and overrideTypedText

>3

+

substituteKeyboardEvents (legacy, use overrideKeystroke and overrideTypedText instead)

+ +

Should be able to prevent cut, typing, and pasting in this field: 1+2+3

+ +

Should wrap anything you type in '<>': 1+2+3

+ +

+

    +
  • [Firefox] Should not change '3' to '33' when you select all and press right arrow.
  • +
  • [All Browsers] ctrl-c or cmd-c should not throw an error
  • + +
3 +

+ From ac78a347da194a615488b25ab137989d8e30a0d3 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Fri, 18 May 2018 11:14:34 -0400 Subject: [PATCH 248/393] Use Math Input instead of MathQuill Input for default ARIA label --- docs/Api_Methods.md | 4 ++-- src/commands/math.js | 2 +- src/controller.js | 2 +- src/publicapi.js | 4 ++-- src/services/latex.js | 2 +- src/services/textarea.js | 2 +- test/unit/aria.test.js | 8 ++++---- test/unit/publicapi.test.js | 2 +- 8 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docs/Api_Methods.md b/docs/Api_Methods.md index 82370d650..364a75730 100644 --- a/docs/Api_Methods.md +++ b/docs/Api_Methods.md @@ -223,11 +223,11 @@ mathField.typedText('x=-b\\pm \\sqrt b^2 -4ac'); ## .setAriaLabel(ariaLabel) -Specify an [ARIA label][`aria-label`] for this field, for screen readers. The actual [`aria-label`] includes this label followed by the math content of the field as speech. Default: `'MathQuill Input'` +Specify an [ARIA label][`aria-label`] for this field, for screen readers. The actual [`aria-label`] includes this label followed by the math content of the field as speech. Default: `'Math Input'` ## .getAriaLabel() -Returns the [ARIA label][`aria-label`] for this field, for screen readers. If no ARIA label has been specified, `'MathQuill Input'` is returned. +Returns the [ARIA label][`aria-label`] for this field, for screen readers. If no ARIA label has been specified, `'Math Input'` is returned. ## .setAriaPostLabel(ariaPostLabel) diff --git a/src/commands/math.js b/src/commands/math.js index 5ec477cd7..614ec95ca 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -522,7 +522,7 @@ API.StaticMath = function(APIClasses) { }; _.setAriaLabel = function(ariaLabel) { this.__controller.ariaLabel = typeof ariaLabel === 'string' ? ariaLabel : ''; - var prependedLabel = this.__controller.ariaLabel !== 'MathQuill Input' ? this.__controller.ariaLabel + ': ' : ''; + var prependedLabel = this.__controller.ariaLabel !== 'Math Input' ? this.__controller.ariaLabel + ': ' : ''; this.__controller.container.attr('aria-label', prependedLabel + this.__controller.root.mathspeak().trim()); return this; }; diff --git a/src/controller.js b/src/controller.js index 1e39059b6..e4f3daf50 100644 --- a/src/controller.js +++ b/src/controller.js @@ -15,7 +15,7 @@ var Controller = P(function(_) { this.container = container; this.options = options; - this.ariaLabel = 'MathQuill Input'; + this.ariaLabel = 'Math Input'; this.ariaPostLabel = ''; root.controller = this; diff --git a/src/publicapi.js b/src/publicapi.js index 638d854ac..83b527dc9 100644 --- a/src/publicapi.js +++ b/src/publicapi.js @@ -222,11 +222,11 @@ function getInterface(v) { }; _.setAriaLabel = function(ariaLabel) { if(ariaLabel && typeof ariaLabel === 'string' && ariaLabel!='') this.__controller.ariaLabel = ariaLabel; - else this.__controller.ariaLabel = 'MathQuill Input'; + else this.__controller.ariaLabel = 'Math Input'; return this; }; _.getAriaLabel = function () { - return this.__controller.ariaLabel || 'MathQuill Input'; + return this.__controller.ariaLabel || 'Math Input'; }; _.setAriaPostLabel = function(ariaPostLabel) { if(ariaPostLabel && typeof ariaPostLabel === 'string' && ariaPostLabel!='') this.__controller.ariaPostLabel = ariaPostLabel; diff --git a/src/services/latex.js b/src/services/latex.js index 6e03b32e8..16b13b83f 100644 --- a/src/services/latex.js +++ b/src/services/latex.js @@ -280,7 +280,7 @@ Controller.open(function(_, super_) { else { jQ.empty(); } - var prependedLabel = this.ariaLabel && this.ariaLabel !== 'MathQuill Input' ? this.ariaLabel + ': ' : ''; + var prependedLabel = this.ariaLabel && this.ariaLabel !== 'Math Input' ? this.ariaLabel + ': ' : ''; this.container.attr('aria-label', prependedLabel + root.mathspeak().trim()); delete cursor.selection; diff --git a/src/services/textarea.js b/src/services/textarea.js index 44f302d74..75dd632f5 100644 --- a/src/services/textarea.js +++ b/src/services/textarea.js @@ -72,7 +72,7 @@ Controller.open(function(_) { textarea.val(text); if (text) textarea.select(); }; - var ariaLabel = ctrlr && ctrlr.ariaLabel !== 'MathQuill Input' ? ctrlr.ariaLabel + ': ' : ''; + var ariaLabel = ctrlr && ctrlr.ariaLabel !== 'Math Input' ? ctrlr.ariaLabel + ': ' : ''; ctrlr.container.attr('aria-label', ariaLabel + root.mathspeak().trim()); }; Options.p.substituteKeyboardEvents = saneKeyboardEvents; diff --git a/test/unit/aria.test.js b/test/unit/aria.test.js index acc6f52c0..b4ecd4da8 100644 --- a/test/unit/aria.test.js +++ b/test/unit/aria.test.js @@ -90,21 +90,21 @@ suite('aria', function() { mathField.keystroke('End'); assertAriaEqual('end of block "s" "q" "r" "t" left parenthesis, "x" , right parenthesis'); mathField.keystroke('Ctrl-Home'); - assertAriaEqual('beginning of MathQuill Input "s" "q" "r" "t" left parenthesis, "x" , right parenthesis'); + assertAriaEqual('beginning of Math Input "s" "q" "r" "t" left parenthesis, "x" , right parenthesis'); mathField.keystroke('Ctrl-End'); - assertAriaEqual('end of MathQuill Input "s" "q" "r" "t" left parenthesis, "x" , right parenthesis'); + assertAriaEqual('end of Math Input "s" "q" "r" "t" left parenthesis, "x" , right parenthesis'); }); test('testing aria-label for interactive and static math', function(done) { mathField.typedText('sqrt(x)'); mathField.blur(); setTimeout(function() { - assert.equal(mathField.__controller.container.attr('aria-label'), 'MathQuill Input: "s" "q" "r" "t" left parenthesis, "x" , right parenthesis'); + assert.equal(mathField.__controller.container.attr('aria-label'), 'Math Input: "s" "q" "r" "t" left parenthesis, "x" , right parenthesis'); done(); }); var staticMath = MQ.StaticMath($('y=\\frac{2x}{3y}').appendTo('#mock')[0]); assert.equal('"y" equals StartFraction, 2 "x" Over 3 "y" , EndFraction', staticMath.__controller.container.attr('aria-label')); - assert.equal('MathQuill Input', staticMath.getAriaLabel()); + assert.equal('Math Input', staticMath.getAriaLabel()); staticMath.setAriaLabel('Static Label'); assert.equal('Static Label: "y" equals StartFraction, 2 "x" Over 3 "y" , EndFraction', staticMath.__controller.container.attr('aria-label')); assert.equal('Static Label', staticMath.getAriaLabel()); diff --git a/test/unit/publicapi.test.js b/test/unit/publicapi.test.js index 76823be16..ff78c9b08 100644 --- a/test/unit/publicapi.test.js +++ b/test/unit/publicapi.test.js @@ -165,7 +165,7 @@ suite('Public API', function() { assert.equal(mq.getAriaPostLabel(), 'ARIA post-label'); mq.setAriaLabel(''); mq.setAriaPostLabel(''); - assert.equal(mq.getAriaLabel(), 'MathQuill Input'); + assert.equal(mq.getAriaLabel(), 'Math Input'); assert.equal(mq.getAriaPostLabel(), ''); }); From 4d629818e948145276dbf156163460386f467c99 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Fri, 18 May 2018 12:51:48 -0400 Subject: [PATCH 249/393] Allow MathQuill to generate ARIA alerts for ariaPostLabel updates --- docs/Api_Methods.md | 4 +++- src/publicapi.js | 21 ++++++++++++++++++--- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/Api_Methods.md b/docs/Api_Methods.md index 364a75730..15e358bdf 100644 --- a/docs/Api_Methods.md +++ b/docs/Api_Methods.md @@ -229,10 +229,12 @@ Specify an [ARIA label][`aria-label`] for this field, for screen readers. The ac Returns the [ARIA label][`aria-label`] for this field, for screen readers. If no ARIA label has been specified, `'Math Input'` is returned. -## .setAriaPostLabel(ariaPostLabel) +## .setAriaPostLabel(ariaPostLabel, timeout) Specify a suffix to be appended to the [ARIA label][`aria-label`], after the math content of the field. Default: `''` (empty string) +If a timeout (in ms) is supplied, the content of the MathQuill and the suffix will be spoken aloud by a screen reader if the field has keyboard focus. + ## .getAriaPostLabel() Returns the suffix to be appended to the [ARIA label][`aria-label`], after the math content of the field. If no ARIA post-label has been specified, `''` (empty string) is returned. diff --git a/src/publicapi.js b/src/publicapi.js index 83b527dc9..613118076 100644 --- a/src/publicapi.js +++ b/src/publicapi.js @@ -228,9 +228,24 @@ function getInterface(v) { _.getAriaLabel = function () { return this.__controller.ariaLabel || 'Math Input'; }; - _.setAriaPostLabel = function(ariaPostLabel) { - if(ariaPostLabel && typeof ariaPostLabel === 'string' && ariaPostLabel!='') this.__controller.ariaPostLabel = ariaPostLabel; - else this.__controller.ariaPostLabel = ''; + _.setAriaPostLabel = function(ariaPostLabel, timeout) { + var controller = this.__controller; + if(ariaPostLabel && typeof ariaPostLabel === 'string' && ariaPostLabel!='') { + if ( + ariaPostLabel !== controller.ariaPostLabel && + typeof timeout === 'number' + ) { + if (this.ariaAlertTimeout) clearTimeout(this.ariaAlertTimeout); + this.ariaAlertTimeout = setTimeout(function() { + if (!!$(document.activeElement).closest($(controller.container)).length) { + aria.alert(this.mathspeak().trim() + ' ' + ariaPostLabel.trim()); + } + }.bind(this), timeout); + } + controller.ariaPostLabel = ariaPostLabel; + } else { + controller.ariaPostLabel = ''; + } return this; }; _.getAriaPostLabel = function () { From a265703d3dcf98610b508527b6bb0d7354ba8ee8 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Fri, 18 May 2018 13:49:33 -0400 Subject: [PATCH 250/393] Be sure to clear ARIA timeout if blank or invalid ARIA post-label is encountered --- src/publicapi.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/publicapi.js b/src/publicapi.js index 613118076..fdcf969a2 100644 --- a/src/publicapi.js +++ b/src/publicapi.js @@ -244,6 +244,7 @@ function getInterface(v) { } controller.ariaPostLabel = ariaPostLabel; } else { + if (this.ariaAlertTimeout) clearTimeout(this.ariaAlertTimeout); controller.ariaPostLabel = ''; } return this; From 2280e5ae195d47cbf3fdda14b8f590149029a98c Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Mon, 21 May 2018 11:59:01 -0400 Subject: [PATCH 251/393] Tweak documentation --- docs/Api_Methods.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/Api_Methods.md b/docs/Api_Methods.md index 15e358bdf..1608870a7 100644 --- a/docs/Api_Methods.md +++ b/docs/Api_Methods.md @@ -233,7 +233,7 @@ Returns the [ARIA label][`aria-label`] for this field, for screen readers. If no Specify a suffix to be appended to the [ARIA label][`aria-label`], after the math content of the field. Default: `''` (empty string) -If a timeout (in ms) is supplied, the content of the MathQuill and the suffix will be spoken aloud by a screen reader if the field has keyboard focus. +If a timeout (in ms) is supplied, and the math field has keyboard focus when the time has elapsed, an ARIA alert will fire which will cause a screen reader to read the content of the field along with the ARIA post-label. This is useful if the post-label contains an evaluation, error message, or other text that the user needs to know about. ## .getAriaPostLabel() From 68920f00d4c81148316d14e73c3e9bdcd57d897b Mon Sep 17 00:00:00 2001 From: Eli Luberoff Date: Mon, 30 Jul 2018 16:11:18 -0500 Subject: [PATCH 252/393] use semi-transparency instead of gray --- src/css/math.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/css/math.less b/src/css/math.less index e31f1fe25..d6b172955 100644 --- a/src/css/math.less +++ b/src/css/math.less @@ -37,7 +37,7 @@ // TODO: what's the difference between these? .mq-empty { - background: #ccc; + background: rgba(0,0,0,.2); &.mq-root-block { background: transparent; } @@ -203,7 +203,7 @@ -o-transform-origin: center .06em; transform-origin: center .06em; - &.mq-ghost { color: silver; } + &.mq-ghost { color: rgba(0,0,0, .2) } + span { margin-top: .1em; From 4de7714a44ccb71f33b5ad047a8fdcb207d4a2a8 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Wed, 5 Sep 2018 14:01:43 -0400 Subject: [PATCH 253/393] Improve ARIA label assignment for lower and upper bounds in summation notation We were previously setting the label only when the initial MathBlock tree was constructed due to LaTeX being set externally. It now occurs in the finalizeTree method which makes navigating through summation notation through the calculator much easier to comprehend with a screen reader. --- src/commands/math/commands.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 125a0b0a2..12621f5fc 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -372,17 +372,6 @@ var SummationNotation = P(MathCommand, function(_, super_) { var self = this; var blocks = self.blocks = [ MathBlock(), MathBlock() ]; for (var i = 0; i < blocks.length; i += 1) { - switch(i) { - case 0: - blocks[i].ariaLabel = 'lower bound'; - break; - case 1: - blocks[i].ariaLabel = 'upper bound'; - break; - default: // Presumably we shouldn't hit this, but one never knows. - blocks[i].ariaLabel = 'block ' + i; - break; - } blocks[i].adopt(self, self.ends[R], 0); } @@ -395,6 +384,8 @@ 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]; From 28fefa989a95769ffe85e702c8a53608a777450f Mon Sep 17 00:00:00 2001 From: Eli Luberoff Date: Fri, 5 Oct 2018 12:48:32 -0700 Subject: [PATCH 254/393] switch sum and product symbols --- src/commands/math/commands.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 12621f5fc..592a59f29 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -393,11 +393,11 @@ var SummationNotation = P(MathCommand, function(_, super_) { }; }); -LatexCmds['∏'] = +LatexCmds['∑'] = LatexCmds.sum = LatexCmds.summation = bind(SummationNotation,'\\sum ','∑', 'sum'); -LatexCmds['∑'] = +LatexCmds['∏'] = LatexCmds.prod = LatexCmds.product = bind(SummationNotation,'\\prod ','∏', 'product'); From 4c693bc586675932f0fc8636bc523f640a6432c3 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Mon, 15 Oct 2018 22:21:11 -0400 Subject: [PATCH 255/393] hide the gray rectangle within empty parens --- src/commands/math.js | 7 +++++-- src/css/math.less | 4 +++- src/tree.js | 6 ++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/commands/math.js b/src/commands/math.js index 614ec95ca..e796c31d0 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -486,9 +486,12 @@ var MathBlock = P(MathElement, function(_, super_) { }; _.blur = function() { this.jQ.removeClass('mq-hasCursor'); - if (this.isEmpty()) + if (this.isEmpty()) { this.jQ.addClass('mq-empty'); - + if (this.isEmptyParens()) { + this.jQ.addClass('mq-empty-parens'); + } + } return this; }; }); diff --git a/src/css/math.less b/src/css/math.less index d6b172955..8b51dde09 100644 --- a/src/css/math.less +++ b/src/css/math.less @@ -41,13 +41,15 @@ &.mq-root-block { background: transparent; } + &.mq-empty-parens { + background: transparent + } } &.mq-empty { background: transparent; } - .mq-text-mode { display: inline-block; } diff --git a/src/tree.js b/src/tree.js index 6214d447b..2712eda35 100644 --- a/src/tree.js +++ b/src/tree.js @@ -259,6 +259,12 @@ var Node = P(function(_) { return this.ends[L] === 0 && this.ends[R] === 0; }; + _.isEmptyParens = function () { + if (!this.isEmpty()) return false; + if (!this.parent) return false; + return this.parent.ctrlSeq === '\\left('; + } + _.isStyleBlock = function() { return false; }; From fbc1f2f72feead12feac97b8004688eef7fd2859 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Wed, 21 Nov 2018 17:51:25 -0500 Subject: [PATCH 256/393] normalize selection across browsers. Always use a light blue background and do not invert the color of text. --- src/css/selections.less | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/css/selections.less b/src/css/selections.less index 930de6c62..1bf31164b 100644 --- a/src/css/selections.less +++ b/src/css/selections.less @@ -12,9 +12,6 @@ .mq-selection { &, & .mq-non-leaf, & .mq-scaled { background: #B4D5FE !important; - background: Highlight !important; - color: HighlightText; - border-color: HighlightText; } .mq-matrixed { From 5d67abc4dae1192fed18c4329b55ea67084e802e Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Thu, 22 Nov 2018 00:24:18 -0500 Subject: [PATCH 257/393] use SVG symbols instead of transforms --- src/commands/math/commands.js | 133 ++++++++++++++++++++++++++++------ src/css/math.less | 39 +++++----- 2 files changed, 130 insertions(+), 42 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 592a59f29..d2d659034 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -1,6 +1,84 @@ /*************************** * Commands and Operators. **************************/ +var SVG_SYMBOLS = { + 'sqrt': { + html: + '' + + '' + + '' + }, + '|': { + width: '.4em', + html: + '' + + '' + + '' + }, + '[': { + width: '.55em', + html: + '' + + '' + + '' + }, + ']': { + width: '.55em', + html: + '' + + '' + + '' + }, + '(': { + width: '.55em', + html: + '' + + '' + + '' + }, + ')': { + width: '.55em', + html: + '' + + '' + + '' + }, + '{': { + width: '.7em', + html: + '' + + '' + + '' + }, + '}': { + width: '.7em', + html: + '' + + '' + + '' + }, + '∥': { + width: '.7em', + html: + '' + + '' + + '' + }, + '⟩': { + width: '.55em', + html: + '' + + '' + + '' + }, + '⟨': { + width: '.55em', + html: + '' + + '' + + '' + } +}; var scale, // = function(jQ, x, y) { ... } //will use a CSS 2D transform to scale the jQuery-wrapped HTML elements, @@ -525,8 +603,10 @@ LatexCmds.sqrt = LatexCmds['√'] = P(MathCommand, function(_, super_) { _.ctrlSeq = '\\sqrt'; _.htmlTemplate = - '' - + '' + '' + + '' + + SVG_SYMBOLS.sqrt.html + + '' + '&0' + '' ; @@ -544,10 +624,6 @@ LatexCmds['√'] = P(MathCommand, function(_, super_) { }); }).or(super_.parser.call(this)); }; - _.reflow = function() { - var block = this.ends[R].jQ; - scale(block.prev(), 1, block.innerHeight()/+block.css('fontSize').slice(0,-2) - .1); - }; }); var Hat = LatexCmds.hat = P(MathCommand, function(_, super_) { @@ -565,8 +641,10 @@ var NthRoot = LatexCmds.nthroot = P(SquareRoot, function(_, super_) { _.htmlTemplate = '&0' - + '' - + '' + + '' + + '' + + SVG_SYMBOLS.sqrt.html + + '' + '&1' + '' ; @@ -602,11 +680,6 @@ function DelimsMixin(_, super_) { this.delimjQs = this.jQ.children(':first').add(this.jQ.children(':last')); this.contentjQ = this.jQ.children(':eq(1)'); }; - _.reflow = function() { - var height = this.contentjQ.outerHeight() - / parseFloat(this.contentjQ.css('fontSize')); - scale(this.delimjQs, min(1 + .2*(height - 1), 1.2), 1.2*height); - }; } // Round/Square/Curly/Angle Brackets (aka Parens/Brackets/Braces) @@ -619,17 +692,20 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) { this.sides = {}; this.sides[L] = { ch: open, ctrlSeq: ctrlSeq }; this.sides[R] = { ch: close, ctrlSeq: end }; + + this.leftSymbol = SVG_SYMBOLS[open]; + this.rightSymbol = SVG_SYMBOLS[close]; }; _.numBlocks = function() { return 1; }; _.html = function() { // wait until now so that .side may this.htmlTemplate = // be set by createLeftOf or parser - '' - + '' - + this.sides[L].ch + '' + + '' + + this.leftSymbol.html + '' - + '&0' - + '' - + this.sides[R].ch + + '&0' + + '' + + this.rightSymbol.html + '' + '' ; @@ -666,8 +742,9 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) { _.closeOpposing = function(brack) { brack.side = 0; brack.sides[this.side] = this.sides[this.side]; // copy over my info (may be - brack.delimjQs.eq(this.side === L ? 0 : 1) // mismatched, like [a, b)) - .removeClass('mq-ghost').html(this.sides[this.side].ch); + var jq = brack.delimjQs.eq(this.side === L ? 0 : 1) // mismatched, like [a, b)) + .removeClass('mq-ghost'); + this.rebuildSymbol(jq, this.side); }; _.createLeftOf = function(cursor) { if (!this.replacedFragment) { // unless wrapping seln in brackets, @@ -743,8 +820,9 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) { else { // else deleting just one of a pair of brackets, become one-sided this.sides[side] = { ch: OPP_BRACKS[this.sides[this.side].ch], ctrlSeq: OPP_BRACKS[this.sides[this.side].ctrlSeq] }; - this.delimjQs.removeClass('mq-ghost') - .eq(side === L ? 0 : 1).addClass('mq-ghost').html(this.sides[side].ch); + var $jq = this.delimjQs.removeClass('mq-ghost') + .eq(side === L ? 0 : 1).addClass('mq-ghost'); + this.rebuildSymbol($jq, side); } if (sib) { // auto-expand so ghost is at far end var origEnd = this.ends[L].ends[side]; @@ -758,6 +836,15 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) { : cursor.insAtDirEnd(side, this.ends[L])); } }; + _.rebuildSymbol = function ($brack, side) { + if (side === L) { + $brack.html(this.leftSymbol.html).css('width', this.leftSymbol.width); + $brack.next().css('margin-left', this.leftSymbol.width); + } else { + $brack.html(this.rightSymbol.html).css('width', this.rightSymbol.width); + $brack.prev().css('margin-right', this.rightSymbol.width); + } + }; _.deleteTowards = function(dir, cursor) { this.deleteSide(-dir, false, cursor); }; diff --git a/src/css/math.less b/src/css/math.less index 8b51dde09..681bf6f7d 100644 --- a/src/css/math.less +++ b/src/css/math.less @@ -196,21 +196,18 @@ //// // parentheses - .mq-paren { - padding: 0 .1em; - vertical-align: top; - -webkit-transform-origin: center .06em; - -moz-transform-origin: center .06em; - -ms-transform-origin: center .06em; - -o-transform-origin: center .06em; - transform-origin: center .06em; - - &.mq-ghost { color: rgba(0,0,0, .2) } - - + span { - margin-top: .1em; - margin-bottom: .1em; - } + .mq-ghost { fill: rgba(0,0,0, .2) } + .mq-bracket-middle { + margin-top: .1em; + margin-bottom: .1em; + } + .mq-bracket { + position: absolute; + top: 0; + bottom: 2px; + } + .mq-bracket-container { + position: relative; } .mq-array { @@ -280,16 +277,20 @@ // \sqrt // square roots .mq-sqrt-prefix { - padding-top: 0; - position: relative; + position: absolute; top: 0.1em; - vertical-align: top; - .transform-origin(top); + bottom: 0.15em; + width: 0.95em; + } + + .mq-sqrt-container { + position: relative; } .mq-sqrt-stem { border-top: 1px solid; margin-top: 1px; + margin-left: 0.9em; padding-left: .15em; padding-right: .2em; margin-right: .1em; From 5017af978c6a95785620c07d9cd33aff3a09ae60 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Thu, 22 Nov 2018 00:29:16 -0500 Subject: [PATCH 258/393] remove code for scaling brackets --- src/commands/math/commands.js | 62 ----------------------------------- src/services/textarea.js | 1 - 2 files changed, 63 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index d2d659034..39c6a9033 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -80,68 +80,6 @@ var SVG_SYMBOLS = { } }; -var scale, // = function(jQ, x, y) { ... } -//will use a CSS 2D transform to scale the jQuery-wrapped HTML elements, -//or the filter matrix transform fallback for IE 5.5-8, or gracefully degrade to -//increasing the fontSize to match the vertical Y scaling factor. - -//ideas from http://github.com/louisremi/jquery.transform.js -//see also http://msdn.microsoft.com/en-us/library/ms533014(v=vs.85).aspx - - forceIERedraw = noop, - div = document.createElement('div'), - div_style = div.style, - transformPropNames = { - transform:1, - WebkitTransform:1, - MozTransform:1, - OTransform:1, - msTransform:1 - }, - transformPropName; - -for (var prop in transformPropNames) { - if (prop in div_style) { - transformPropName = prop; - break; - } -} - -if (transformPropName) { - scale = function(jQ, x, y) { - jQ.css(transformPropName, 'scale('+x+','+y+')'); - }; -} -else if ('filter' in div_style) { //IE 6, 7, & 8 fallback, see https://github.com/laughinghan/mathquill/wiki/Transforms - forceIERedraw = function(el){ el.className = el.className; }; - scale = function(jQ, x, y) { //NOTE: assumes y > x - x /= (1+(y-1)/2); - jQ.css('fontSize', y + 'em'); - if (!jQ.hasClass('mq-matrixed-container')) { - jQ.addClass('mq-matrixed-container') - .wrapInner(''); - } - var innerjQ = jQ.children() - .css('filter', 'progid:DXImageTransform.Microsoft' - + '.Matrix(M11=' + x + ",SizingMethod='auto expand')" - ); - function calculateMarginRight() { - jQ.css('marginRight', (innerjQ.width()-1)*(x-1)/x + 'px'); - } - calculateMarginRight(); - var intervalId = setInterval(calculateMarginRight); - $(window).load(function() { - clearTimeout(intervalId); - calculateMarginRight(); - }); - }; -} -else { - scale = function(jQ, x, y) { - jQ.css('fontSize', y + 'em'); - }; -} - var Style = P(MathCommand, function(_, super_) { _.init = function(ctrlSeq, tagName, attrs) { super_.init.call(this, ctrlSeq, '<'+tagName+' '+attrs+'>&0'); diff --git a/src/services/textarea.js b/src/services/textarea.js index 75dd632f5..ea162e671 100644 --- a/src/services/textarea.js +++ b/src/services/textarea.js @@ -21,7 +21,6 @@ Controller.open(function(_) { }; _.selectionChanged = function() { var ctrlr = this; - forceIERedraw(ctrlr.container[0]); // throttle calls to setTextareaSelection(), because setting textarea.value // and/or calling textarea.select() can have anomalously bad performance: From 2fdb56ce2f338bbb356256b34353c2d46e235ba7 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Thu, 22 Nov 2018 00:40:57 -0500 Subject: [PATCH 259/393] fix \\lang and \\rrang mixup --- src/commands/math/commands.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 39c6a9033..91421d792 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -64,14 +64,14 @@ var SVG_SYMBOLS = { '' + '' }, - '⟩': { + '⟨': { width: '.55em', html: '' + '' + '' }, - '⟨': { + '⟩': { width: '.55em', html: '' + From 8e5017f314515eb30efcd2b9c459bd7d0b7e5349 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Thu, 22 Nov 2018 01:42:40 -0500 Subject: [PATCH 260/393] automatically use the current font color for svg symbol fill --- src/css/math.less | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/css/math.less b/src/css/math.less index 681bf6f7d..98d927a18 100644 --- a/src/css/math.less +++ b/src/css/math.less @@ -7,6 +7,12 @@ overflow: hidden; vertical-align: middle; } +.mq-root-block svg { + // svg symbols are sometimes used for autoscaling brackets and + // square root symbols. This piece of css magic allows you to copy + // over the current value of the font color to the svg symbols. + fill: currentColor; +} .mq-math-mode { font-variant: normal; font-weight: normal; From 28471ffd055d229fd1406d77561a4d898d2703a7 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Thu, 22 Nov 2018 02:20:50 -0500 Subject: [PATCH 261/393] fixup sqrt sign on large fonts --- src/css/math.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/css/math.less b/src/css/math.less index 98d927a18..29dec25e6 100644 --- a/src/css/math.less +++ b/src/css/math.less @@ -284,7 +284,7 @@ // square roots .mq-sqrt-prefix { position: absolute; - top: 0.1em; + top: 1px; bottom: 0.15em; width: 0.95em; } From 2586cae6f003fd28a60c476814f7b73043586531 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Thu, 22 Nov 2018 10:10:11 -0500 Subject: [PATCH 262/393] make sure ghost parens show gray --- src/css/math.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/css/math.less b/src/css/math.less index 29dec25e6..e98c3a36e 100644 --- a/src/css/math.less +++ b/src/css/math.less @@ -202,7 +202,7 @@ //// // parentheses - .mq-ghost { fill: rgba(0,0,0, .2) } + .mq-ghost svg { fill: rgba(0,0,0, .2) } .mq-bracket-middle { margin-top: .1em; margin-bottom: .1em; From 99ec778a02b4e35efb22e46a20c58964542f7447 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Thu, 22 Nov 2018 12:11:29 -0500 Subject: [PATCH 263/393] ghost parens use currentColor. also cleanup .replaceBracket --- src/commands/math/commands.js | 41 ++++++++++++++++++++--------------- src/css/math.less | 10 +++++++-- 2 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 91421d792..dba7d482c 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -630,25 +630,29 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) { this.sides = {}; this.sides[L] = { ch: open, ctrlSeq: ctrlSeq }; this.sides[R] = { ch: close, ctrlSeq: end }; - - this.leftSymbol = SVG_SYMBOLS[open]; - this.rightSymbol = SVG_SYMBOLS[close]; }; _.numBlocks = function() { return 1; }; - _.html = function() { // wait until now so that .side may + _.html = function() { + var leftSymbol = this.getSymbol(L); + var rightSymbol = this.getSymbol(R); + + // wait until now so that .side may this.htmlTemplate = // be set by createLeftOf or parser '' - + '' - + this.leftSymbol.html + + '' + + leftSymbol.html + '' - + '&0' - + '' - + this.rightSymbol.html + + '&0' + + '' + + rightSymbol.html + '' + '' ; return super_.html.call(this); }; + _.getSymbol = function (side) { + return SVG_SYMBOLS[this.sides[side || R].ch] || {width: '0', html: ''}; + }; _.latex = function() { return '\\left'+this.sides[L].ctrlSeq+this.ends[L].latex()+'\\right'+this.sides[R].ctrlSeq; }; @@ -680,9 +684,9 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) { _.closeOpposing = function(brack) { brack.side = 0; brack.sides[this.side] = this.sides[this.side]; // copy over my info (may be - var jq = brack.delimjQs.eq(this.side === L ? 0 : 1) // mismatched, like [a, b)) + var $brack = brack.delimjQs.eq(this.side === L ? 0 : 1) // mismatched, like [a, b)) .removeClass('mq-ghost'); - this.rebuildSymbol(jq, this.side); + this.replaceBracket($brack, this.side); }; _.createLeftOf = function(cursor) { if (!this.replacedFragment) { // unless wrapping seln in brackets, @@ -758,9 +762,9 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) { 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] }; - var $jq = this.delimjQs.removeClass('mq-ghost') + var $brack = this.delimjQs.removeClass('mq-ghost') .eq(side === L ? 0 : 1).addClass('mq-ghost'); - this.rebuildSymbol($jq, side); + this.replaceBracket($brack, side); } if (sib) { // auto-expand so ghost is at far end var origEnd = this.ends[L].ends[side]; @@ -774,13 +778,14 @@ var Bracket = P(P(MathCommand, DelimsMixin), function(_, super_) { : cursor.insAtDirEnd(side, this.ends[L])); } }; - _.rebuildSymbol = function ($brack, side) { + _.replaceBracket = function ($brack, side) { + var symbol = this.getSymbol(side); + $brack.html(symbol.html).css('width', symbol.width); + if (side === L) { - $brack.html(this.leftSymbol.html).css('width', this.leftSymbol.width); - $brack.next().css('margin-left', this.leftSymbol.width); + $brack.next().css('margin-left', symbol.width); } else { - $brack.html(this.rightSymbol.html).css('width', this.rightSymbol.width); - $brack.prev().css('margin-right', this.rightSymbol.width); + $brack.prev().css('margin-right', symbol.width); } }; _.deleteTowards = function(dir, cursor) { diff --git a/src/css/math.less b/src/css/math.less index e98c3a36e..0ccd2cddf 100644 --- a/src/css/math.less +++ b/src/css/math.less @@ -202,16 +202,22 @@ //// // parentheses - .mq-ghost svg { fill: rgba(0,0,0, .2) } + .mq-ghost svg { opacity: .2 } .mq-bracket-middle { margin-top: .1em; margin-bottom: .1em; } - .mq-bracket { + .mq-bracket-l, .mq-bracket-r { position: absolute; top: 0; bottom: 2px; } + .mq-bracket-l { + left: 0; + } + .mq-bracket-r { + right:0; + } .mq-bracket-container { position: relative; } From 30863ee8e9fd0bfe93c07889568ff11e0fae87d3 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Thu, 22 Nov 2018 12:17:20 -0500 Subject: [PATCH 264/393] add svg width:100% and height:100% through css --- src/commands/math/commands.js | 22 +++++++++++----------- src/css/math.less | 18 ++++++++++++------ 2 files changed, 23 insertions(+), 17 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index dba7d482c..26d8445f3 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -4,77 +4,77 @@ var SVG_SYMBOLS = { 'sqrt': { html: - '' + + '' + '' + '' }, '|': { width: '.4em', html: - '' + + '' + '' + '' }, '[': { width: '.55em', html: - '' + + '' + '' + '' }, ']': { width: '.55em', html: - '' + + '' + '' + '' }, '(': { width: '.55em', html: - '' + + '' + '' + '' }, ')': { width: '.55em', html: - '' + + '' + '' + '' }, '{': { width: '.7em', html: - '' + + '' + '' + '' }, '}': { width: '.7em', html: - '' + + '' + '' + '' }, '∥': { width: '.7em', html: - '' + + '' + '' + '' }, '⟨': { width: '.55em', html: - '' + + '' + '' + '' }, '⟩': { width: '.55em', html: - '' + + '' + '' + '' } diff --git a/src/css/math.less b/src/css/math.less index 0ccd2cddf..804f6fdb1 100644 --- a/src/css/math.less +++ b/src/css/math.less @@ -7,12 +7,7 @@ overflow: hidden; vertical-align: middle; } -.mq-root-block svg { - // svg symbols are sometimes used for autoscaling brackets and - // square root symbols. This piece of css magic allows you to copy - // over the current value of the font color to the svg symbols. - fill: currentColor; -} + .mq-math-mode { font-variant: normal; font-weight: normal; @@ -31,6 +26,17 @@ line-height: .9; } + svg { + // svg symbols are sometimes used for autoscaling brackets and + // square root symbols. This piece of css magic allows you to copy + // over the current value of the font color to the svg symbols. + fill: currentColor; + + // the svg symbols fill their container + width: 100%; + height: 100%; + } + * { font-size: inherit; line-height: inherit; From ffdbfc5093711e543affdf890c6e67f168245fac Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Thu, 22 Nov 2018 13:06:28 -0500 Subject: [PATCH 265/393] fix \binom command --- src/commands/math/commands.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 26d8445f3..b888cef13 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -886,17 +886,24 @@ LatexCmds.right = P(MathCommand, function(_) { var Binomial = LatexCmds.binom = LatexCmds.binomial = P(P(MathCommand, DelimsMixin), function(_, super_) { + var leftSymbol = SVG_SYMBOLS['(']; + var rightSymbol = SVG_SYMBOLS[')']; + _.ctrlSeq = '\\binom'; _.htmlTemplate = - '' - + '(' - + '' + '' + + '' + + leftSymbol.html + + '' + + '' + '' + '&0' + '&1' + '' + '' - + ')' + + '' + + rightSymbol.html + + '' + '' ; _.textTemplate = ['choose(',',',')']; From 5495b4403d47abf6ca31765cc1f311b2f725d357 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Thu, 22 Nov 2018 20:33:25 -0500 Subject: [PATCH 266/393] remove .transform-origin() mixin --- src/css/mixins/css3.less | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/css/mixins/css3.less b/src/css/mixins/css3.less index b53bf6b93..e1a608aab 100644 --- a/src/css/mixins/css3.less +++ b/src/css/mixins/css3.less @@ -1,10 +1,3 @@ -.transform-origin (...) { - -webkit-transform-origin: @arguments; - -moz-transform-origin: @arguments; - -ms-transform-origin: @arguments; - -o-transform-origin: @arguments; - transform-origin: @arguments; -} .transform (...) { -webkit-transform: @arguments; -moz-transform: @arguments; From 51e6133aff8d1cfa68853dbdad5cc7e72133969d Mon Sep 17 00:00:00 2001 From: Eli Luberoff Date: Sat, 24 Nov 2018 17:36:04 -0800 Subject: [PATCH 267/393] Fix safari layout bug while typing without this, safari wasn't resizing the SVG during typing. Didn't show up in automated tests b/c it would look right on final render. here's the website that pointed towards a fix: https://benfrain.com/attempting-to-fix-responsive-svgs-in-desktop-safari/ (in the comments) --- src/css/math.less | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/css/math.less b/src/css/math.less index 804f6fdb1..6ac4a7bc1 100644 --- a/src/css/math.less +++ b/src/css/math.less @@ -33,6 +33,9 @@ fill: currentColor; // the svg symbols fill their container + position:absolute; + top: 0; + left: 0; width: 100%; height: 100%; } From c976f087ab38868652c1236650fcc1d3efab7d2f Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Fri, 30 Nov 2018 11:14:40 -0500 Subject: [PATCH 268/393] fix "Incorrect Function" error --- src/services/saneKeyboardEvents.util.js | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/services/saneKeyboardEvents.util.js b/src/services/saneKeyboardEvents.util.js index 03826a79d..d86698125 100644 --- a/src/services/saneKeyboardEvents.util.js +++ b/src/services/saneKeyboardEvents.util.js @@ -118,6 +118,16 @@ var saneKeyboardEvents = (function() { } target.bind('keydown keypress input keyup focusout paste', function(e) { checkTextarea(e); }); + function guardedTextareaSelect () { + try { + // IE can throw an 'Incorrect Function' error if you + // try to select a textarea that is hidden. It seems + // likely that we don't really care if the selection + // fails to happen in this case. Why would the textarea + // be hidden? And who would even be able to tell? + textarea[0].select(); + } catch (e) {}; + } // -*- public methods -*- // function select(text) { @@ -129,7 +139,7 @@ var saneKeyboardEvents = (function() { clearTimeout(timeoutId); textarea.val(text); - if (text && textarea[0].select) textarea[0].select(); + if (text) guardedTextareaSelect(); shouldBeSelected = !!text; } var shouldBeSelected = false; @@ -160,10 +170,10 @@ var saneKeyboardEvents = (function() { keypress = null; if (shouldBeSelected) checkTextareaOnce(function(e) { - if (!(e && e.type === 'focusout') && textarea[0].select) { + 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 - textarea[0].select(); + guardedTextareaSelect() } }); @@ -245,7 +255,7 @@ var saneKeyboardEvents = (function() { } } // in Firefox, keys that don't type text, just clear seln, fire keypress // https://github.com/mathquill/mathquill/issues/293#issuecomment-40997668 - else if (text && textarea[0].select) textarea[0].select(); // re-select if that's why we're here + else if (text) guardedTextareaSelect(); // re-select if that's why we're here } function onBlur() { keydown = keypress = null; } @@ -284,7 +294,7 @@ var saneKeyboardEvents = (function() { copy: function(e) { e.preventDefault(); }, cut: function(e) { e.preventDefault(); }, paste: function(e) { e.preventDefault(); } - }); + }); } else { target.bind({ keydown: onKeydown, @@ -294,7 +304,7 @@ var saneKeyboardEvents = (function() { cut: function() { checkTextareaOnce(function() { controller.cut(); }); }, copy: function() { checkTextareaOnce(function() { controller.copy(); }); }, paste: onPaste - }); + }); } // -*- export public methods -*- // From 8d4c90d839dd1c1516ed037719b571f4658a2791 Mon Sep 17 00:00:00 2001 From: Eli Luberoff Date: Wed, 12 Dec 2018 16:12:53 -0800 Subject: [PATCH 269/393] add infinity --- src/commands/math/basicSymbols.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index 97a9b5ccf..03ff1a9c5 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -647,6 +647,8 @@ LatexCmds['<'] = LatexCmds.lt = bind(Inequality, less, true); LatexCmds['>'] = LatexCmds.gt = bind(Inequality, greater, true); LatexCmds['≤'] = LatexCmds.le = LatexCmds.leq = bind(Inequality, less, false); LatexCmds['≥'] = LatexCmds.ge = LatexCmds.geq = bind(Inequality, greater, false); +LatexCmds.infty = LatexCmds.infin = LatexCmds.infinity = + bind(VanillaSymbol,'\\infty ','∞', 'infinity'); var Equality = P(BinaryOperator, function(_, super_) { _.init = function() { From 098acc7febbf78c0da01ab225a9a75df50c36194 Mon Sep 17 00:00:00 2001 From: Eli Luberoff Date: Wed, 12 Dec 2018 16:29:28 -0800 Subject: [PATCH 270/393] rm inf from advancedSymbols --- src/commands/math/advancedSymbols.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/commands/math/advancedSymbols.js b/src/commands/math/advancedSymbols.js index 771597438..050013ffe 100644 --- a/src/commands/math/advancedSymbols.js +++ b/src/commands/math/advancedSymbols.js @@ -289,9 +289,6 @@ LatexCmds.image = LatexCmds.imagin = LatexCmds.imaginary = LatexCmds.Imaginary = LatexCmds.part = LatexCmds.partial = bind(VanillaSymbol,'\\partial ','∂', 'partial'); -LatexCmds.infty = LatexCmds.infin = LatexCmds.infinity = - bind(VanillaSymbol,'\\infty ','∞', 'infinity'); - LatexCmds.alef = LatexCmds.alefsym = LatexCmds.aleph = LatexCmds.alephsym = bind(VanillaSymbol,'\\aleph ','ℵ', 'alef sym'); From 5c448f7a6a0a2a310a6de4151dc5cf1491de827e Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Tue, 8 Jan 2019 13:40:06 -0500 Subject: [PATCH 271/393] fix scrollHoriz test --- src/publicapi.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/publicapi.js b/src/publicapi.js index fdcf969a2..94f101017 100644 --- a/src/publicapi.js +++ b/src/publicapi.js @@ -178,8 +178,9 @@ function getInterface(v) { else /* TODO: API needs better error reporting */; } else cursor.parent.write(cursor, cmd); - if (ctrlr.blurred) cursor.hide().parent.blur(); + ctrlr.scrollHoriz(); + if (ctrlr.blurred) cursor.hide().parent.blur(); return this; }; _.select = function() { From 51c349280cb4a6a3dee1aeba07a7cfbff4b11a50 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Tue, 8 Jan 2019 14:03:45 -0500 Subject: [PATCH 272/393] fix text html tests by using dynamic ids --- test/unit/text.test.js | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/test/unit/text.test.js b/test/unit/text.test.js index 44e8cd773..c2150aa0f 100644 --- a/test/unit/text.test.js +++ b/test/unit/text.test.js @@ -100,20 +100,27 @@ suite('text', function() { test('HTML for subclassed text blocks', function() { var block = fromLatex('\\text{abc}'); - assert.equal(block.html(), 'abc'); + var _id = block.html().match(/mathquill-command-id=([0-9]+)/)[1]; + function id () { + _id = parseInt(_id) + 3; + return _id; + } + + block = fromLatex('\\text{abc}'); + assert.equal(block.html(), 'abc'); block = fromLatex('\\textit{abc}'); - assert.equal(block.html(), 'abc'); + assert.equal(block.html(), 'abc'); block = fromLatex('\\textbf{abc}'); - assert.equal(block.html(), 'abc'); + assert.equal(block.html(), 'abc'); block = fromLatex('\\textsf{abc}'); - assert.equal(block.html(), 'abc'); + assert.equal(block.html(), 'abc'); block = fromLatex('\\texttt{abc}'); - assert.equal(block.html(), 'abc'); + assert.equal(block.html(), 'abc'); block = fromLatex('\\textsc{abc}'); - assert.equal(block.html(), 'abc'); + assert.equal(block.html(), 'abc'); block = fromLatex('\\uppercase{abc}'); - assert.equal(block.html(), 'abc'); + assert.equal(block.html(), 'abc'); block = fromLatex('\\lowercase{abc}'); - assert.equal(block.html(), 'abc'); + assert.equal(block.html(), 'abc'); }); }); From bf4e89090f3a1f5f1c07ce52f1b3ae0ceb4a6e4c Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Tue, 8 Jan 2019 15:41:44 -0500 Subject: [PATCH 273/393] do not add spacing between letters and period --- src/commands/math/basicSymbols.js | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index 03ff1a9c5..39d7023cb 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -235,8 +235,19 @@ var Letter = P(Variable, function(_, super_) { } }; function shouldOmitPadding(node) { - // omit padding if no node, or if node already has padding (to avoid double-padding) - return !node || (node instanceof BinaryOperator) || (node instanceof SummationNotation); + // omit padding if no node + if (!node) return true; + + // do not add padding between letter and '.' + if (node.ctrlSeq === '.') return true; + + // do not add padding between letter and binary operator. The + // binary operator already has padding + if (node instanceof BinaryOperator) return true; + + if (node instanceof SummationNotation) return true; + + return false; } }); var BuiltInOpNames = {}; // the set of operator names like \sin, \cos, etc that From 68747275cbb61ae9ba3f733a93ad05b44dbbef86 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Tue, 8 Jan 2019 18:22:29 -0500 Subject: [PATCH 274/393] space prevents binary operator. (2, -2) won't put padding around minus sign --- src/commands/math/basicSymbols.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index 39d7023cb..15d0cead1 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -588,10 +588,10 @@ var PlusMinus = P(BinaryOperator, function(_) { _.contactWeld = _.siblingCreated = _.siblingDeleted = function(opts, dir) { function determineOpClassType(node) { if (node[L]) { - // If the left sibling is a binary operator or a separator (comma, semicolon, colon) + // 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 (node[L] instanceof BinaryOperator || /^(\\ )|[,;:\(\[]$/.test(node[L].ctrlSeq)) { return ''; } } else if (node.parent && node.parent.parent && node.parent.parent.isStyleBlock()) { From e821fadc220a0a0bb8618ef8700fbf8faaae0a45 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Wed, 9 Jan 2019 13:18:55 -0500 Subject: [PATCH 275/393] fix mouse selection of parens and sqrts within denominators --- src/services/mouse.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/services/mouse.js b/src/services/mouse.js index f7ed68c26..dd6725850 100644 --- a/src/services/mouse.js +++ b/src/services/mouse.js @@ -107,28 +107,28 @@ Controller.open(function(_) { }); Controller.open(function(_) { - _.seek = function(target, pageX, pageY) { + _.seek = function($target, pageX, pageY) { var cursor = this.notify('select').cursor; var node; - var targetElm = target && target[0]; + var targetElm = $target && $target[0]; - // try to find the node by the target - if (targetElm) { + // 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); + if (node) break; - // if that didn't work find the node by the target's parent - if (!node) { - node = Node.getNodeOfElement(target.parentElement); - } + // must be too deep, traverse up to the parent DOM Element + targetElm = targetElm.parentElement; } - // if that didn't work then the root is the node + // Could not find any nodes, just use the root if (!node) { node = this.root; } - pray('nodeId is the id of some Node that exists', node); - // don't clear selection until after getting node from target, in case // target was selection span, otherwise target will have no parent and will // seek from root, which is less accurate (e.g. fraction) From 3f966ad5b37d03008f50ff296a3697ca41bbd782 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Wed, 9 Jan 2019 14:59:06 -0500 Subject: [PATCH 276/393] stop trying to process keyboard actions on blur We are seeing cases where blur is tricking mathquill into thinking there was a keypress. The hypothesis is that mathquill missed a character in the textarea for some reason (or failed to clear it) and didn't check again until blur. At that point it finds a character in the textarea and thinks it was a typed character. This then causes a dispatch-in-dispatch error because a completely unrelated dispatch was the cause of the blur. I can't think of a time when we'd rather read a character out late compared to just ignoring the character. I think when it comes to keyboard input it's always safer to miss characters than make characters up. We should let this soak for a while to make sure this doesn't cause issues. I'm hoping to see a noticeable decrease in dispatch-in-dispatch errors over the next few days. --- src/services/saneKeyboardEvents.util.js | 12 ++++++++++-- test/unit/saneKeyboardEvents.test.js | 6 +++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/src/services/saneKeyboardEvents.util.js b/src/services/saneKeyboardEvents.util.js index d86698125..93a61a691 100644 --- a/src/services/saneKeyboardEvents.util.js +++ b/src/services/saneKeyboardEvents.util.js @@ -116,7 +116,9 @@ var saneKeyboardEvents = (function() { checker(e); }); } - target.bind('keydown keypress input keyup focusout paste', function(e) { checkTextarea(e); }); + target.bind('keydown keypress input keyup paste', function(e) { + checkTextarea(e); + }); function guardedTextareaSelect () { try { @@ -258,7 +260,13 @@ var saneKeyboardEvents = (function() { else if (text) guardedTextareaSelect(); // re-select if that's why we're here } - function onBlur() { keydown = keypress = null; } + function onBlur() { + keydown = null; + keypress = null; + checkTextarea = noop; + clearTimeout(timeoutId); + textarea.val(''); + } function onPaste(e) { // browsers are dumb. diff --git a/test/unit/saneKeyboardEvents.test.js b/test/unit/saneKeyboardEvents.test.js index 467084a1d..0f024df04 100644 --- a/test/unit/saneKeyboardEvents.test.js +++ b/test/unit/saneKeyboardEvents.test.js @@ -148,11 +148,11 @@ suite('saneKeyboardEvents', function() { // IE < 9 doesn't support selection{Start,End} if (supportsSelectionAPI()) { - assert.equal(el[0].selectionStart, 0, 'it\'s selected from the start'); - assert.equal(el[0].selectionEnd, 6, 'it\'s selected to the end'); + assert.equal(el[0].selectionStart, 0, 'it is not selected at the start'); + assert.equal(el[0].selectionEnd, 0, 'it is not selected at the end'); } - assert.equal(el.val(), 'foobar', 'it still has content'); + assert.equal(el.val(), '', 'it has no content'); }); test('blur then empty selection', function() { From 460288be81d638f0073ba0d464b3bf48e8c97301 Mon Sep 17 00:00:00 2001 From: Eli Luberoff Date: Mon, 28 Jan 2019 13:30:04 -0700 Subject: [PATCH 277/393] add neq support --- src/commands/math/advancedSymbols.js | 2 -- src/commands/math/basicSymbols.js | 2 ++ 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/math/advancedSymbols.js b/src/commands/math/advancedSymbols.js index 050013ffe..a1fe98dd1 100644 --- a/src/commands/math/advancedSymbols.js +++ b/src/commands/math/advancedSymbols.js @@ -12,8 +12,6 @@ LatexCmds.otimes = P(BinaryOperator, function(_, super_) { }; }); -LatexCmds['≠'] = LatexCmds.ne = LatexCmds.neq = bind(BinaryOperator,'\\ne ','≠', 'not equal'); - LatexCmds['∗'] = LatexCmds.ast = LatexCmds.star = LatexCmds.loast = LatexCmds.lowast = bind(BinaryOperator,'\\ast ','∗', 'low asterisk'); LatexCmds.therefor = LatexCmds.therefore = diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index 15d0cead1..77ee94ff1 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -660,6 +660,8 @@ LatexCmds['≤'] = LatexCmds.le = LatexCmds.leq = bind(Inequality, less, false); LatexCmds['≥'] = LatexCmds.ge = LatexCmds.geq = bind(Inequality, greater, false); LatexCmds.infty = LatexCmds.infin = LatexCmds.infinity = bind(VanillaSymbol,'\\infty ','∞', 'infinity'); +LatexCmds['≠'] = LatexCmds.ne = LatexCmds.neq = bind(BinaryOperator,'\\ne ','≠', 'not equal'); + var Equality = P(BinaryOperator, function(_, super_) { _.init = function() { From 17fae0d5dad25514a4a103e428a27211d2a7e493 Mon Sep 17 00:00:00 2001 From: Eli Luberoff Date: Fri, 8 Feb 2019 07:51:38 -0800 Subject: [PATCH 278/393] add more auto superscripts --- src/commands/math/basicSymbols.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index 15d0cead1..9de347ba4 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -575,9 +575,17 @@ 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'); From 1bc06ba7310eae902ba5ee7461cf53413ec5699d Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Wed, 13 Feb 2019 12:15:55 -0500 Subject: [PATCH 279/393] Fix invisible static math on iOS with VoiceOver Ensure that we assign role='math' to staticMath fields, and be sure to assign an initial ARIA label to new instances of MQ.mathFields if not focused immediately after creation. --- src/services/focusBlur.js | 2 ++ src/services/textarea.js | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/focusBlur.js b/src/services/focusBlur.js index 0fc791bb6..7c5b5d3da 100644 --- a/src/services/focusBlur.js +++ b/src/services/focusBlur.js @@ -35,6 +35,7 @@ Controller.open(function(_) { clearTimeout(blurTimeout); // tabs/windows, not intentional blur if (cursor.selection) cursor.selection.jQ.addClass('mq-blur'); blur(); + updateAria(); } function blur() { // not directly in the textarea blur handler so as to be cursor.hide().parent.blur(); // synchronous with/in the same frame as @@ -51,6 +52,7 @@ Controller.open(function(_) { ctrlr.textarea.attr('aria-label', mqAria); ctrlr.container.attr('aria-label', mqAria); } + updateAria(); ctrlr.blurred = true; cursor.hide().parent.blur(); }; diff --git a/src/services/textarea.js b/src/services/textarea.js index ea162e671..0b7d5eeba 100644 --- a/src/services/textarea.js +++ b/src/services/textarea.js @@ -72,7 +72,7 @@ Controller.open(function(_) { if (text) textarea.select(); }; var ariaLabel = ctrlr && ctrlr.ariaLabel !== 'Math Input' ? ctrlr.ariaLabel + ': ' : ''; - ctrlr.container.attr('aria-label', ariaLabel + root.mathspeak().trim()); + ctrlr.container.attr('role', 'math').attr('aria-label', ariaLabel + root.mathspeak().trim()); }; Options.p.substituteKeyboardEvents = saneKeyboardEvents; _.editablesTextareaEvents = function() { From c17b526478005d98825852941f5a14ddf1838b4b Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Tue, 26 Mar 2019 19:42:05 -0400 Subject: [PATCH 280/393] add spaces between groups of digits --- src/commands/math/basicSymbols.js | 166 +++++++++++++++++++++++++++++- src/controller.js | 2 +- src/css/math.less | 38 +++++++ src/cursor.js | 3 +- src/services/focusBlur.js | 30 ++++++ src/services/latex.js | 9 ++ test/demo.html | 1 + test/digit-grouping.html | 86 ++++++++++++++++ 8 files changed, 331 insertions(+), 4 deletions(-) create mode 100644 test/digit-grouping.html diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index 47c446b67..44a54fbbb 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -1,8 +1,160 @@ /********************************* * Symbols for Basic Mathematics ********************************/ +var DigitGroupingChar = P(Symbol, function(_, super_) { + _.finalizeTree = _.siblingDeleted = _.siblingCreated = function(opts, dir) { + // 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) { + if (!opts.enableDigitGrouping) return; + + var left = this; + var right = this; + + var spacesFound = 0; + var dots = []; + + var SPACE = '\\ '; + var DOT = '.'; + + // traverse left as far as possible (starting at this char) + var node = left; + do { + if (/^[0-9]$/.test(node.ctrlSeq)) { + left = node + } else if (node.ctrlSeq === SPACE) { + left = node + spacesFound += 1; + } else if (node.ctrlSeq === DOT) { + left = node + dots.push(node); + } else { + break; + } + } while (node = left[L]); + + // traverse right as far as possible (starting to right of this char) + while (node = right[R]) { + if (/^[0-9]$/.test(node.ctrlSeq)) { + right = node + } else if (node.ctrlSeq === SPACE) { + right = node + spacesFound += 1; + } else if (node.ctrlSeq === DOT) { + right = node + dots.push(node); + } else { + break; + } + } + + // trim the leading spaces + while (right !== left && left.ctrlSeq === SPACE) { + left = left[R]; + spacesFound -= 1; + } + + // trim the trailing spaces + while (right !== left && right.ctrlSeq === SPACE) { + right = right[L]; + spacesFound -= 1; + } + + // happens when you only have a space + if (left === right && left.ctrlSeq === SPACE) return; + + var disableFormatting = spacesFound > 0 || dots.length > 1; + if (disableFormatting) { + this.removeGroupingBetween(left, right); + } else if (dots[0]) { + if (dots[0] !== left) { + this.addGroupingBetween(dots[0][L], left); + } + if (dots[0] !== right) { + // we do not show grouping to the right of a decimal place #yet + this.removeGroupingBetween(dots[0][R], right); + } + } else { + this.addGroupingBetween(right, left); + } + }; + + _.removeGroupingBetween = function (left, right) { + var node = left; + do { + node.setGroupingClass(undefined); + if (node === right) break; + } while (node = node[R]); + }; + + _.addGroupingBetween = function (start, end) { + var node = start; + var count = 0; + + var totalDigits = 0; + var node = start; + while (node) { + totalDigits += 1; + + if (node === end) break; + node = node[L]; + } + + var numDigitsInFirstGroup = totalDigits % 3; + if (numDigitsInFirstGroup === 0) numDigitsInFirstGroup = 3; + + var node = start; + while (node) { + count += 1; + + cls = undefined; + + // only do grouping if we have at least 4 numbers + if (totalDigits >= 4) { + if (count === totalDigits) { + cls = 'mq-group-leading-' + numDigitsInFirstGroup; + } else if (count % 3 === 0) { + if (count !== totalDigits) { + cls = 'mq-group-start' + } + } + + if (!cls) { + cls = 'mq-group-other' + } + } + + node.setGroupingClass(cls); + + if (node === end) break; + node = node[L]; + } + }; + + _.setGroupingClass = function (cls) { + // nothing changed (either class is the same or it's still undefined) + if (this._groupingClass === cls) return; + + // remove existing class + if (this._groupingClass) this.jQ.removeClass(this._groupingClass); + + // add new class + if (cls) this.jQ.addClass(cls); + + // 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); + }; -var Digit = P(VanillaSymbol, function(_, super_) { _.createLeftOf = function(cursor) { if (cursor.options.autoSubscriptNumerals && cursor.parent !== cursor.parent.parent.sub @@ -376,7 +528,17 @@ LatexCmds.f = P(Letter, function(_, super_) { }); // VanillaSymbol's -LatexCmds[' '] = LatexCmds.space = bind(VanillaSymbol, '\\ ', ' ', 'space'); +LatexCmds[' '] = LatexCmds.space = P(DigitGroupingChar, function(_, super_) { + _.init = function () { + super_.init.call(this, '\\ ', ' ', ' '); + }; +}); + +LatexCmds['.'] = P(DigitGroupingChar, function(_, super_) { + _.init = function () { + super_.init.call(this, '.', '.', '.'); + }; +}); LatexCmds["'"] = LatexCmds.prime = bind(VanillaSymbol, "'", '′', 'prime'); LatexCmds['″'] = LatexCmds.dprime = bind(VanillaSymbol, '″', '″', 'double prime'); diff --git a/src/controller.js b/src/controller.js index e4f3daf50..5138a8759 100644 --- a/src/controller.js +++ b/src/controller.js @@ -20,7 +20,7 @@ var Controller = P(function(_) { root.controller = this; - this.cursor = root.cursor = Cursor(root, options); + this.cursor = root.cursor = Cursor(root, options, this); // TODO: stop depending on root.cursor, and rm it }; diff --git a/src/css/math.less b/src/css/math.less index 6ac4a7bc1..0422925f4 100644 --- a/src/css/math.less +++ b/src/css/math.less @@ -1,3 +1,9 @@ +// look here to see the digit layout strategy: +// https://www.desmos.com/calculator/ctvh9utz0t +@digit-separator: .11em; +@expand-margin: .009em; +@contract-margin: -.01em; + .mq-root-block, .mq-math-mode .mq-root-block { .inline-block; width: 100%; @@ -6,6 +12,38 @@ white-space: nowrap; overflow: hidden; vertical-align: middle; + + .mq-digit { + margin-left: @expand-margin; + margin-right: @expand-margin; + } + + .mq-group-start { + margin-left: @digit-separator; + margin-right: @contract-margin; + } + + .mq-group-other { + margin-left: @contract-margin; + margin-right: @contract-margin; + } + + .mq-group-leading-1, .mq-group-leading-2 { + margin-left: 0; + margin-right: @contract-margin; + } + + .mq-group-leading-3 { + margin-left: 4 * @expand-margin; + margin-right: @contract-margin; + } + + &.mq-suppress-grouping { + .mq-group-start, .mq-group-other, .mq-group-leading-1, .mq-group-leading-2, .mq-group-leading-3 { + margin-left: @expand-margin; + margin-right: @expand-margin; + } + } } .mq-math-mode { diff --git a/src/cursor.js b/src/cursor.js index 30c29d251..615e2993b 100644 --- a/src/cursor.js +++ b/src/cursor.js @@ -11,7 +11,8 @@ 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) { + _.init = function(initParent, options, controller) { + this.controller = controller; this.parent = initParent; this.options = options; diff --git a/src/services/focusBlur.js b/src/services/focusBlur.js index 7c5b5d3da..1b78da0c9 100644 --- a/src/services/focusBlur.js +++ b/src/services/focusBlur.js @@ -1,4 +1,33 @@ 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; + + // blurred === false means we are focused. blurred === true or + // blurred === undefined means we are not focused. + if (controller.blurred !== false) return; + + controller.disableGroupingForSeconds(1); + } + }); + + _.disableGroupingForSeconds = function (seconds) { + clearTimeout(this.__disableGroupingTimeout); + var jQ = this.root.jQ; + + if (seconds === 0) { + jQ.removeClass('mq-suppress-grouping'); + } else { + jQ.addClass('mq-suppress-grouping'); + this.__disableGroupingTimeout = setTimeout(function () { + jQ.removeClass('mq-suppress-grouping'); + }, seconds * 1000); + } + } + _.focusBlurEvents = function() { var ctrlr = this, root = ctrlr.root, cursor = ctrlr.cursor; var blurTimeout; @@ -21,6 +50,7 @@ Controller.open(function(_) { clearTimeout(ctrlr.textareaSelectionTimeout); ctrlr.textareaSelectionTimeout = undefined; } + ctrlr.disableGroupingForSeconds(0); ctrlr.blurred = true; blurTimeout = setTimeout(function() { // wait for blur on window; if root.postOrder(function (node) { node.intentionalBlur(); }); // none, intentional blur: #264 diff --git a/src/services/latex.js b/src/services/latex.js index 16b13b83f..0e7ecf314 100644 --- a/src/services/latex.js +++ b/src/services/latex.js @@ -18,6 +18,7 @@ var latexMathParser = (function() { var string = Parser.string; var regex = Parser.regex; var letter = Parser.letter; + var digit = Parser.digit; var any = Parser.any; var optWhitespace = Parser.optWhitespace; var succeed = Parser.succeed; @@ -26,6 +27,7 @@ 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 controlSequence = @@ -49,6 +51,7 @@ var latexMathParser = (function() { var command = controlSequence .or(variable) + .or(number) .or(symbol) ; @@ -254,6 +257,12 @@ Controller.open(function(_, super_) { } this.cursor.resetToEnd(this); + + var rightMost = root.ends[R]; + if (rightMost.fixDigitGrouping) { + rightMost.fixDigitGrouping(this.cursor.options); + } + return true; }; _.renderLatexMathFromScratch = function (latex) { diff --git a/test/demo.html b/test/demo.html index c9ce7e9ff..9b8f9005c 100644 --- a/test/demo.html +++ b/test/demo.html @@ -93,6 +93,7 @@

MathQuill Demo local test p $(latexMath.el()).bind('keydown keypress', function() { setTimeout(function() { var latex = latexMath.latex(); + latexSource.val(latex); // location.hash = '#'+latex; //extremely performance-crippling in Chrome for some reason htmlSource.text(printTree(latexMath.html())); diff --git a/test/digit-grouping.html b/test/digit-grouping.html new file mode 100644 index 000000000..276879645 --- /dev/null +++ b/test/digit-grouping.html @@ -0,0 +1,86 @@ + + + + + + + +MathQuill Digit Grouping Demo + + + + + + +
+ +Fork me on GitHub! + +

MathQuill Digit Grouping Demo

+ + +

Grouping Disabled

+

-12345678 + .23232323 + 23232.23233 + +

Grouping Enabled

+

-12345678 + .23232323 + 23232.23233 + +

Optimized latex updates

+

-12345678.2342342 + +

Edge Cases

+

+ +

+ + + + + From c95ceef9fa771e1ea2793e9fbcf05b7e62ba1e06 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Thu, 11 Apr 2019 14:14:24 -0400 Subject: [PATCH 281/393] start adding digit grouping tests --- test/unit/digit-grouping.test.js | 355 +++++++++++++++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 test/unit/digit-grouping.test.js diff --git a/test/unit/digit-grouping.test.js b/test/unit/digit-grouping.test.js new file mode 100644 index 000000000..3a3b5712d --- /dev/null +++ b/test/unit/digit-grouping.test.js @@ -0,0 +1,355 @@ +suite('Digit Grouping', function() { + + function buildTreeRecursively ($el) { + var tree = {}; + + if ($el[0].className) { + tree.classes = $el[0].className; + } + + var children = $el.children(); + if (children.length) { + tree.content = []; + for (var i=0; i < children.length; i++) { + tree.content.push(buildTreeRecursively($(children[i]))); + } + } else { + tree.content = $el[0].innerHTML; + } + + return tree; + } + + function assertClasses (mq, expected) { + var $el = $(mq.el()); + var actual = { + latex: mq.latex(), + suppressedGrouping: $el.hasClass('mq-suppress-grouping'), + tree: buildTreeRecursively($el.find('.mq-root-block')) + }; + + window.actual = actual; + assert.equal(JSON.stringify(actual, null, 2), JSON.stringify(expected, null, 2)); + } + + test('edge cases', function () { + var mq = MQ.MathField($('').appendTo('#mock')[0], {enableDigitGrouping: true}); + assertClasses(mq, { + latex: '', + suppressedGrouping: false, + tree: { + classes: 'mq-root-block mq-empty', + content: '' + } + }) + + mq.latex('1\\ '); + assertClasses(mq, { + latex: '1\\ ', + suppressedGrouping: false, + tree: { + classes: 'mq-root-block', + content: [ + { + classes: 'mq-digit', + content: '1' + }, + { + content: ' ', + } + ], + } + }); + + mq.latex('\\ 1'); + assertClasses(mq, { + "latex": "\\ 1", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "content": " " + }, + { + "classes": "mq-digit", + "content": "1" + } + ] + } + }); + + mq.latex('\\ 1\\ '); + assertClasses(mq, { + "latex": "\\ 1\\ ", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "content": " " + }, + { + "classes": "mq-digit", + "content": "1" + }, + { + "content": " " + } + ] + } + }); + + mq.latex('a'); + assertClasses(mq, { + "latex": "a", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "content": "a" + } + ] + } + }); + + mq.latex('a\\ '); + assertClasses(mq, { + "latex": "a\\ ", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "content": "a" + }, + { + "content": " " + } + ] + } + }); + + mq.latex('\\ a'); + assertClasses(mq, { + "latex": "\\ a", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "content": " " + }, + { + "content": "a" + } + ] + } + }); + + mq.latex('a\\ a'); + assertClasses(mq, { + "latex": "a\\ a", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "content": "a" + }, + { + "content": " " + }, + { + "content": "a" + } + ] + } + }); + + mq.latex('\\ a\\ '); + assertClasses(mq, { + "latex": "\\ a\\ ", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "content": " " + }, + { + "content": "a" + }, + { + "content": " " + } + ] + } + }); + + mq.latex('.'); + assertClasses(mq, { + "latex": ".", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "classes": "mq-digit", + "content": "." + } + ] + } + }); + + mq.latex('.\\ .'); + assertClasses(mq, { + "latex": ".\\ .", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "classes": "mq-digit", + "content": "." + }, + { + "content": " " + }, + { + "classes": "mq-digit", + "content": "." + } + ] + } + }); + + mq.latex('..'); + assertClasses(mq, { + "latex": "..", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "classes": "mq-digit", + "content": "." + }, + { + "classes": "mq-digit", + "content": "." + } + ] + } + }); + + mq.latex('2..'); + assertClasses(mq, { + "latex": "2..", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "classes": "mq-digit", + "content": "2" + }, + { + "classes": "mq-digit", + "content": "." + }, + { + "content": "." + } + ] + } + }); + + mq.latex('..2'); + assertClasses(mq, { + "latex": "..2", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "classes": "mq-digit", + "content": "." + }, + { + "classes": "mq-digit", + "content": "." + }, + { + "content": "2" + } + ] + } + }); + + mq.latex('\\ \\ '); + assertClasses(mq, { + "latex": "\\ \\ ", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "content": " " + }, + { + "content": " " + } + ] + } + }); + + mq.latex('\\ \\ \\ '); + assertClasses(mq, { + "latex": "\\ \\ \\ ", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "content": " " + }, + { + "content": " " + }, + { + "content": " " + } + ] + } + }); + + mq.latex('1234'); + assertClasses(mq, { + "latex": "1234", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "classes": "mq-digit mq-group-leading-1", + "content": "1" + }, + { + "classes": "mq-digit mq-group-start", + "content": "2" + }, + { + "classes": "mq-digit mq-group-other", + "content": "3" + }, + { + "classes": "mq-digit mq-group-other", + "content": "4" + } + ] + } + }); + }); +}); \ No newline at end of file From d3beccbea0aeaeb424ed75b3db29d258231e03b1 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Thu, 11 Apr 2019 14:23:30 -0400 Subject: [PATCH 282/393] test that grouping can be disabled --- test/unit/digit-grouping.test.js | 360 +++++++++++++++++++++++++++++++ 1 file changed, 360 insertions(+) diff --git a/test/unit/digit-grouping.test.js b/test/unit/digit-grouping.test.js index 3a3b5712d..96e684004 100644 --- a/test/unit/digit-grouping.test.js +++ b/test/unit/digit-grouping.test.js @@ -352,4 +352,364 @@ suite('Digit Grouping', function() { } }); }); + + test('efficient latex updates - grouping enabled', function () { + var mq = MQ.MathField($('').appendTo('#mock')[0], {enableDigitGrouping: true}); + assertClasses(mq, { + latex: '', + suppressedGrouping: false, + tree: { + classes: 'mq-root-block mq-empty', + content: '' + } + }) + + mq.latex('1.2322'); + assertClasses(mq, { + "latex": "1.2322", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "classes": "mq-digit", + "content": "1" + }, + { + "classes": "mq-digit", + "content": "." + }, + { + "classes": "mq-digit", + "content": "2" + }, + { + "classes": "mq-digit", + "content": "3" + }, + { + "classes": "mq-digit", + "content": "2" + }, + { + "classes": "mq-digit", + "content": "2" + } + ] + } + }); + + mq.latex('1231.123'); + assertClasses(mq, { + "latex": "1231.123", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "classes": "mq-digit mq-group-leading-1", + "content": "1" + }, + { + "classes": "mq-digit mq-group-start", + "content": "2" + }, + { + "classes": "mq-digit mq-group-other", + "content": "3" + }, + { + "classes": "mq-digit mq-group-other", + "content": "1" + }, + { + "classes": "mq-digit", + "content": "." + }, + { + "classes": "mq-digit", + "content": "1" + }, + { + "content": "2" + }, + { + "content": "3" + } + ] + } + }); + + mq.latex('1231.432'); + assertClasses(mq, { + "latex": "1231.432", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "classes": "mq-digit mq-group-leading-1", + "content": "1" + }, + { + "classes": "mq-digit mq-group-start", + "content": "2" + }, + { + "classes": "mq-digit mq-group-other", + "content": "3" + }, + { + "classes": "mq-digit mq-group-other", + "content": "1" + }, + { + "classes": "mq-digit", + "content": "." + }, + { + "classes": "mq-digit", + "content": "4" + }, + { + "content": "3" + }, + { + "content": "2" + } + ] + } + }); + + mq.latex('1231232.432'); + assertClasses(mq, { + "latex": "1231232.432", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "classes": "mq-digit mq-group-leading-1", + "content": "1" + }, + { + "classes": "mq-digit mq-group-start", + "content": "2" + }, + { + "classes": "mq-digit mq-group-other", + "content": "3" + }, + { + "classes": "mq-digit mq-group-other", + "content": "1" + }, + { + "classes": "mq-digit mq-group-start", + "content": "2" + }, + { + "classes": "mq-digit mq-group-other", + "content": "3" + }, + { + "classes": "mq-group-other", + "content": "2" + }, + { + "content": "." + }, + { + "content": "4" + }, + { + "content": "3" + }, + { + "content": "2" + } + ] + } + }); + }); + + test('efficient latex updates - grouping disabled', function () { + var mq = MQ.MathField($('').appendTo('#mock')[0]); + assertClasses(mq, { + latex: '', + suppressedGrouping: false, + tree: { + classes: 'mq-root-block mq-empty', + content: '' + } + }) + + mq.latex('1.2322'); + assertClasses(mq, { + "latex": "1.2322", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "classes": "mq-digit", + "content": "1" + }, + { + "classes": "mq-digit", + "content": "." + }, + { + "classes": "mq-digit", + "content": "2" + }, + { + "classes": "mq-digit", + "content": "3" + }, + { + "classes": "mq-digit", + "content": "2" + }, + { + "classes": "mq-digit", + "content": "2" + } + ] + } + }); + + mq.latex('1231.123'); + assertClasses(mq, { + "latex": "1231.123", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "classes": "mq-digit", + "content": "1" + }, + { + "classes": "mq-digit", + "content": "2" + }, + { + "classes": "mq-digit", + "content": "3" + }, + { + "classes": "mq-digit", + "content": "1" + }, + { + "classes": "mq-digit", + "content": "." + }, + { + "classes": "mq-digit", + "content": "1" + }, + { + "content": "2" + }, + { + "content": "3" + } + ] + } + }); + + mq.latex('1231.432'); + assertClasses(mq, { + "latex": "1231.432", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "classes": "mq-digit", + "content": "1" + }, + { + "classes": "mq-digit", + "content": "2" + }, + { + "classes": "mq-digit", + "content": "3" + }, + { + "classes": "mq-digit", + "content": "1" + }, + { + "classes": "mq-digit", + "content": "." + }, + { + "classes": "mq-digit", + "content": "4" + }, + { + "content": "3" + }, + { + "content": "2" + } + ] + } + }); + + mq.latex('1231232.432'); + assertClasses(mq, { + "latex": "1231232.432", + "suppressedGrouping": false, + "tree": { + "classes": "mq-root-block", + "content": [ + { + "classes": "mq-digit", + "content": "1" + }, + { + "classes": "mq-digit", + "content": "2" + }, + { + "classes": "mq-digit", + "content": "3" + }, + { + "classes": "mq-digit", + "content": "1" + }, + { + "classes": "mq-digit", + "content": "2" + }, + { + "classes": "mq-digit", + "content": "3" + }, + { + "content": "2" + }, + { + "content": "." + }, + { + "content": "4" + }, + { + "content": "3" + }, + { + "content": "2" + } + ] + } + }); + }); + }); \ No newline at end of file From af61db5ceb5c49c7d36a835c4dd72807536de43c Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Thu, 11 Apr 2019 14:44:16 -0400 Subject: [PATCH 283/393] make sure to add .mq-digit to digits efficiently created --- src/services/latex.js | 1 + test/unit/digit-grouping.test.js | 21 ++++++++++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/src/services/latex.js b/src/services/latex.js index 0e7ecf314..4495ec9cc 100644 --- a/src/services/latex.js +++ b/src/services/latex.js @@ -233,6 +233,7 @@ Controller.open(function(_, super_) { for (i = commonLength; i < newDigits.length; i++) { var span = document.createElement('span'); + span.className = "mq-digit"; span.textContent = newDigits[i]; var newNode = Digit(newDigits[i]); diff --git a/test/unit/digit-grouping.test.js b/test/unit/digit-grouping.test.js index 96e684004..6b4cd8644 100644 --- a/test/unit/digit-grouping.test.js +++ b/test/unit/digit-grouping.test.js @@ -260,6 +260,7 @@ suite('Digit Grouping', function() { "content": "." }, { + "classes": "mq-digit", "content": "." } ] @@ -282,6 +283,7 @@ suite('Digit Grouping', function() { "content": "." }, { + "classes": "mq-digit", "content": "2" } ] @@ -431,9 +433,11 @@ suite('Digit Grouping', function() { "content": "1" }, { + "classes": "mq-digit", "content": "2" }, { + "classes": "mq-digit", "content": "3" } ] @@ -472,9 +476,11 @@ suite('Digit Grouping', function() { "content": "4" }, { + "classes": "mq-digit", "content": "3" }, { + "classes": "mq-digit", "content": "2" } ] @@ -513,19 +519,23 @@ suite('Digit Grouping', function() { "content": "3" }, { - "classes": "mq-group-other", + "classes": "mq-digit mq-group-other", "content": "2" }, { + "classes": "mq-digit", "content": "." }, { + "classes": "mq-digit", "content": "4" }, { + "classes": "mq-digit", "content": "3" }, { + "classes": "mq-digit", "content": "2" } ] @@ -611,9 +621,11 @@ suite('Digit Grouping', function() { "content": "1" }, { + "classes": "mq-digit", "content": "2" }, { + "classes": "mq-digit", "content": "3" } ] @@ -652,9 +664,11 @@ suite('Digit Grouping', function() { "content": "4" }, { + "classes": "mq-digit", "content": "3" }, { + "classes": "mq-digit", "content": "2" } ] @@ -693,18 +707,23 @@ suite('Digit Grouping', function() { "content": "3" }, { + "classes": "mq-digit", "content": "2" }, { + "classes": "mq-digit", "content": "." }, { + "classes": "mq-digit", "content": "4" }, { + "classes": "mq-digit", "content": "3" }, { + "classes": "mq-digit", "content": "2" } ] From 25db9b5817bf7c2bfabbd828be924e9483a7ef9c Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Thu, 11 Apr 2019 14:48:35 -0400 Subject: [PATCH 284/393] test that editing expressions suppresses grouping --- test/unit/digit-grouping.test.js | 281 +++++++++++++++++++++++++++---- 1 file changed, 246 insertions(+), 35 deletions(-) diff --git a/test/unit/digit-grouping.test.js b/test/unit/digit-grouping.test.js index 6b4cd8644..5076042ea 100644 --- a/test/unit/digit-grouping.test.js +++ b/test/unit/digit-grouping.test.js @@ -7,14 +7,18 @@ suite('Digit Grouping', function() { tree.classes = $el[0].className; } - var children = $el.children(); - if (children.length) { - tree.content = []; - for (var i=0; i < children.length; i++) { - tree.content.push(buildTreeRecursively($(children[i]))); - } + if ($el[0].className.indexOf('mq-cursor') !== -1) { + tree.classes = 'mq-cursor'; } else { - tree.content = $el[0].innerHTML; + var children = $el.children(); + if (children.length) { + tree.content = []; + for (var i=0; i < children.length; i++) { + tree.content.push(buildTreeRecursively($(children[i]))); + } + } else { + tree.content = $el[0].innerHTML; + } } return tree; @@ -24,7 +28,6 @@ suite('Digit Grouping', function() { var $el = $(mq.el()); var actual = { latex: mq.latex(), - suppressedGrouping: $el.hasClass('mq-suppress-grouping'), tree: buildTreeRecursively($el.find('.mq-root-block')) }; @@ -36,7 +39,6 @@ suite('Digit Grouping', function() { var mq = MQ.MathField($('').appendTo('#mock')[0], {enableDigitGrouping: true}); assertClasses(mq, { latex: '', - suppressedGrouping: false, tree: { classes: 'mq-root-block mq-empty', content: '' @@ -46,7 +48,6 @@ suite('Digit Grouping', function() { mq.latex('1\\ '); assertClasses(mq, { latex: '1\\ ', - suppressedGrouping: false, tree: { classes: 'mq-root-block', content: [ @@ -64,7 +65,6 @@ suite('Digit Grouping', function() { mq.latex('\\ 1'); assertClasses(mq, { "latex": "\\ 1", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -82,7 +82,6 @@ suite('Digit Grouping', function() { mq.latex('\\ 1\\ '); assertClasses(mq, { "latex": "\\ 1\\ ", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -103,7 +102,6 @@ suite('Digit Grouping', function() { mq.latex('a'); assertClasses(mq, { "latex": "a", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -117,7 +115,6 @@ suite('Digit Grouping', function() { mq.latex('a\\ '); assertClasses(mq, { "latex": "a\\ ", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -134,7 +131,6 @@ suite('Digit Grouping', function() { mq.latex('\\ a'); assertClasses(mq, { "latex": "\\ a", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -151,7 +147,6 @@ suite('Digit Grouping', function() { mq.latex('a\\ a'); assertClasses(mq, { "latex": "a\\ a", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -171,7 +166,6 @@ suite('Digit Grouping', function() { mq.latex('\\ a\\ '); assertClasses(mq, { "latex": "\\ a\\ ", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -191,7 +185,6 @@ suite('Digit Grouping', function() { mq.latex('.'); assertClasses(mq, { "latex": ".", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -206,7 +199,6 @@ suite('Digit Grouping', function() { mq.latex('.\\ .'); assertClasses(mq, { "latex": ".\\ .", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -228,7 +220,6 @@ suite('Digit Grouping', function() { mq.latex('..'); assertClasses(mq, { "latex": "..", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -247,7 +238,6 @@ suite('Digit Grouping', function() { mq.latex('2..'); assertClasses(mq, { "latex": "2..", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -261,6 +251,7 @@ suite('Digit Grouping', function() { }, { "classes": "mq-digit", + "content": "." } ] @@ -270,7 +261,6 @@ suite('Digit Grouping', function() { mq.latex('..2'); assertClasses(mq, { "latex": "..2", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -293,7 +283,6 @@ suite('Digit Grouping', function() { mq.latex('\\ \\ '); assertClasses(mq, { "latex": "\\ \\ ", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -310,7 +299,6 @@ suite('Digit Grouping', function() { mq.latex('\\ \\ \\ '); assertClasses(mq, { "latex": "\\ \\ \\ ", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -330,7 +318,6 @@ suite('Digit Grouping', function() { mq.latex('1234'); assertClasses(mq, { "latex": "1234", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -359,7 +346,6 @@ suite('Digit Grouping', function() { var mq = MQ.MathField($('').appendTo('#mock')[0], {enableDigitGrouping: true}); assertClasses(mq, { latex: '', - suppressedGrouping: false, tree: { classes: 'mq-root-block mq-empty', content: '' @@ -369,7 +355,6 @@ suite('Digit Grouping', function() { mq.latex('1.2322'); assertClasses(mq, { "latex": "1.2322", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -404,7 +389,6 @@ suite('Digit Grouping', function() { mq.latex('1231.123'); assertClasses(mq, { "latex": "1231.123", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -447,7 +431,6 @@ suite('Digit Grouping', function() { mq.latex('1231.432'); assertClasses(mq, { "latex": "1231.432", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -490,7 +473,6 @@ suite('Digit Grouping', function() { mq.latex('1231232.432'); assertClasses(mq, { "latex": "1231232.432", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -547,7 +529,6 @@ suite('Digit Grouping', function() { var mq = MQ.MathField($('').appendTo('#mock')[0]); assertClasses(mq, { latex: '', - suppressedGrouping: false, tree: { classes: 'mq-root-block mq-empty', content: '' @@ -557,7 +538,6 @@ suite('Digit Grouping', function() { mq.latex('1.2322'); assertClasses(mq, { "latex": "1.2322", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -592,7 +572,6 @@ suite('Digit Grouping', function() { mq.latex('1231.123'); assertClasses(mq, { "latex": "1231.123", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -635,7 +614,6 @@ suite('Digit Grouping', function() { mq.latex('1231.432'); assertClasses(mq, { "latex": "1231.432", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -678,7 +656,6 @@ suite('Digit Grouping', function() { mq.latex('1231232.432'); assertClasses(mq, { "latex": "1231232.432", - "suppressedGrouping": false, "tree": { "classes": "mq-root-block", "content": [ @@ -731,4 +708,238 @@ suite('Digit Grouping', function() { }); }); + test('edits suppress digit grouping', function (done) { + var mq = MQ.MathField($('').appendTo('#mock')[0], {enableDigitGrouping: true}); + + assertClasses(mq, { + latex: '', + tree: { + classes: 'mq-root-block mq-empty', + content: '' + } + }) + + $(mq.el()).find('textarea').focus(); + assertClasses(mq, { + latex: '', + tree: { + classes: 'mq-root-block mq-hasCursor', + content: [ + { + classes: "mq-cursor" + } + ] + } + }) + + mq.typedText('1'); + assertClasses(mq, { + latex: '1', + tree: { + classes: 'mq-root-block mq-hasCursor mq-suppress-grouping', + content: [ + { + classes: 'mq-digit', + content: '1' + }, + { + classes: "mq-cursor" + } + ] + } + }) + + mq.typedText('2'); + mq.typedText('3'); + mq.typedText('4'); + assertClasses(mq, { + latex: '1234', + tree: { + classes: 'mq-root-block mq-hasCursor mq-suppress-grouping', + content: [ + { + classes: 'mq-digit mq-group-leading-1', + content: '1' + }, + { + classes: 'mq-digit mq-group-start', + content: '2' + }, + { + classes: 'mq-digit mq-group-other', + content: '3' + }, + { + classes: 'mq-digit mq-group-other', + content: '4' + }, + { + classes: "mq-cursor" + } + ] + } + }) + + mq.typedText('5'); + assertClasses(mq, { + latex: '12345', + tree: { + classes: 'mq-root-block mq-hasCursor mq-suppress-grouping', + content: [ + { + classes: 'mq-digit mq-group-leading-2', + content: '1' + }, + { + classes: 'mq-digit mq-group-other', + content: '2' + }, + { + classes: 'mq-digit mq-group-start', + content: '3' + }, + { + classes: 'mq-digit mq-group-other', + content: '4' + }, + { + classes: 'mq-digit mq-group-other', + content: '5' + }, + { + classes: "mq-cursor" + } + ] + } + }) + + setTimeout(function () { + assertClasses(mq, { + latex: '12345', + tree: { + classes: 'mq-root-block mq-hasCursor', + content: [ + { + classes: 'mq-digit mq-group-leading-2', + content: '1' + }, + { + classes: 'mq-digit mq-group-other', + content: '2' + }, + { + classes: 'mq-digit mq-group-start', + content: '3' + }, + { + classes: 'mq-digit mq-group-other', + content: '4' + }, + { + classes: 'mq-digit mq-group-other', + content: '5' + }, + { + classes: "mq-cursor" + } + ] + } + }) + + mq.keystroke('Left'); + assertClasses(mq, { + latex: '12345', + tree: { + classes: 'mq-root-block mq-hasCursor', + content: [ + { + classes: 'mq-digit mq-group-leading-2', + content: '1' + }, + { + classes: 'mq-digit mq-group-other', + content: '2' + }, + { + classes: 'mq-digit mq-group-start', + content: '3' + }, + { + classes: 'mq-digit mq-group-other', + content: '4' + }, + { + classes: "mq-cursor" + }, + { + classes: 'mq-digit mq-group-other', + content: '5' + }, + ] + } + }) + + mq.keystroke('Backspace'); + assertClasses(mq, { + latex: '1235', + tree: { + classes: 'mq-root-block mq-hasCursor mq-suppress-grouping', + content: [ + { + classes: 'mq-digit mq-group-leading-1', + content: '1' + }, + { + classes: 'mq-digit mq-group-start', + content: '2' + }, + { + classes: 'mq-digit mq-group-other', + content: '3' + }, + { + classes: "mq-cursor" + }, + { + classes: 'mq-digit mq-group-other', + content: '5' + }, + ] + } + }) + + $(mq.el()).find('textarea').blur(); + setTimeout(function () { + assertClasses(mq, { + latex: '1235', + tree: { + classes: 'mq-root-block', + content: [ + { + classes: 'mq-digit mq-group-leading-1', + content: '1' + }, + { + classes: 'mq-digit mq-group-start', + content: '2' + }, + { + classes: 'mq-digit mq-group-other', + content: '3' + }, + { + classes: 'mq-digit mq-group-other', + content: '5' + }, + ] + } + }) + done(); + }, 1); + }, 1100); // should stop suppressing grouping after 1000ms + }); + + test('edits ignored if digit grouping disabled', function () { + + }); }); \ No newline at end of file From ccd0d9560b5699d66439d57e96fc5d97a9b45084 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Thu, 11 Apr 2019 15:18:38 -0400 Subject: [PATCH 285/393] test editing with digit grouping disabled --- test/unit/digit-grouping.test.js | 235 ++++++++++++++++++++++++++++++- 1 file changed, 231 insertions(+), 4 deletions(-) diff --git a/test/unit/digit-grouping.test.js b/test/unit/digit-grouping.test.js index 5076042ea..40a81b01b 100644 --- a/test/unit/digit-grouping.test.js +++ b/test/unit/digit-grouping.test.js @@ -708,6 +708,237 @@ suite('Digit Grouping', function() { }); }); + test('edits ignored if digit grouping disabled', function (done) { + var mq = MQ.MathField($('').appendTo('#mock')[0]); + + assertClasses(mq, { + latex: '', + tree: { + classes: 'mq-root-block mq-empty', + content: '' + } + }) + + $(mq.el()).find('textarea').focus(); + assertClasses(mq, { + latex: '', + tree: { + classes: 'mq-root-block mq-hasCursor', + content: [ + { + classes: "mq-cursor" + } + ] + } + }) + + mq.typedText('1'); + assertClasses(mq, { + latex: '1', + tree: { + classes: 'mq-root-block mq-hasCursor', + content: [ + { + classes: 'mq-digit', + content: '1' + }, + { + classes: "mq-cursor" + } + ] + } + }) + + mq.typedText('2'); + mq.typedText('3'); + mq.typedText('4'); + assertClasses(mq, { + latex: '1234', + tree: { + classes: 'mq-root-block mq-hasCursor', + content: [ + { + classes: 'mq-digit', + content: '1' + }, + { + classes: 'mq-digit', + content: '2' + }, + { + classes: 'mq-digit', + content: '3' + }, + { + classes: 'mq-digit', + content: '4' + }, + { + classes: "mq-cursor" + } + ] + } + }) + + mq.typedText('5'); + assertClasses(mq, { + latex: '12345', + tree: { + classes: 'mq-root-block mq-hasCursor', + content: [ + { + classes: 'mq-digit', + content: '1' + }, + { + classes: 'mq-digit', + content: '2' + }, + { + classes: 'mq-digit', + content: '3' + }, + { + classes: 'mq-digit', + content: '4' + }, + { + classes: 'mq-digit', + content: '5' + }, + { + classes: "mq-cursor" + } + ] + } + }) + + setTimeout(function () { + assertClasses(mq, { + latex: '12345', + tree: { + classes: 'mq-root-block mq-hasCursor', + content: [ + { + classes: 'mq-digit', + content: '1' + }, + { + classes: 'mq-digit', + content: '2' + }, + { + classes: 'mq-digit', + content: '3' + }, + { + classes: 'mq-digit', + content: '4' + }, + { + classes: 'mq-digit', + content: '5' + }, + { + classes: "mq-cursor" + } + ] + } + }) + + mq.keystroke('Left'); + assertClasses(mq, { + latex: '12345', + tree: { + classes: 'mq-root-block mq-hasCursor', + content: [ + { + classes: 'mq-digit', + content: '1' + }, + { + classes: 'mq-digit', + content: '2' + }, + { + classes: 'mq-digit', + content: '3' + }, + { + classes: 'mq-digit', + content: '4' + }, + { + classes: "mq-cursor" + }, + { + classes: 'mq-digit', + content: '5' + }, + ] + } + }) + + mq.keystroke('Backspace'); + assertClasses(mq, { + latex: '1235', + tree: { + classes: 'mq-root-block mq-hasCursor', + content: [ + { + classes: 'mq-digit', + content: '1' + }, + { + classes: 'mq-digit', + content: '2' + }, + { + classes: 'mq-digit', + content: '3' + }, + { + classes: "mq-cursor" + }, + { + classes: 'mq-digit', + content: '5' + }, + ] + } + }) + + $(mq.el()).find('textarea').blur(); + setTimeout(function () { + assertClasses(mq, { + latex: '1235', + tree: { + classes: 'mq-root-block', + content: [ + { + classes: 'mq-digit', + content: '1' + }, + { + classes: 'mq-digit', + content: '2' + }, + { + classes: 'mq-digit', + content: '3' + }, + { + classes: 'mq-digit', + content: '5' + }, + ] + } + }) + done(); + }, 1); + }, 1100); // should stop suppressing grouping after 1000ms + }); + test('edits suppress digit grouping', function (done) { var mq = MQ.MathField($('').appendTo('#mock')[0], {enableDigitGrouping: true}); @@ -938,8 +1169,4 @@ suite('Digit Grouping', function() { }, 1); }, 1100); // should stop suppressing grouping after 1000ms }); - - test('edits ignored if digit grouping disabled', function () { - - }); }); \ No newline at end of file From 0f399657626165698c4ee6c76598ebfc375318e5 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Fri, 12 Apr 2019 22:39:40 -0400 Subject: [PATCH 286/393] remove .mq-matrixed styles. Classes aren't assigned anywhere --- docs/Config.md | 10 ---------- src/css/main.less | 1 - src/css/matrixed.less | 20 -------------------- src/css/selections.less | 31 +------------------------------ test/visual.html | 3 +-- 5 files changed, 2 insertions(+), 63 deletions(-) delete mode 100644 src/css/matrixed.less diff --git a/docs/Config.md b/docs/Config.md index a8f61f7e4..416de41fa 100644 --- a/docs/Config.md +++ b/docs/Config.md @@ -153,13 +153,3 @@ For example, to style as white-on-black instead of black-on-white use: border-color: white; background: black; } - #my-math-input .mq-matrixed { - background: black; - } - #my-math-input .mq-matrixed-container { - filter: progid:DXImageTransform.Microsoft.Chroma(color='black'); - } - -## Color Change Support on IE8 - -To support a MathQuill editable background color other than white in IE8, set the background color on both the editable mathField and on elements with class `mq-matrixed`. Then set a Chroma filter on elements with class `mq-matrixed-container`. diff --git a/src/css/main.less b/src/css/main.less index ba94dd2ed..691e742d5 100644 --- a/src/css/main.less +++ b/src/css/main.less @@ -18,4 +18,3 @@ @import "selections.less"; @import "textarea.less"; -@import "matrixed.less"; diff --git a/src/css/matrixed.less b/src/css/matrixed.less deleted file mode 100644 index 4ebf2d211..000000000 --- a/src/css/matrixed.less +++ /dev/null @@ -1,20 +0,0 @@ -@import "./mixins/display"; - -// We have to set an opaque background color for matrix-stretched -// elements to anti-alias correctly, so we use the Chroma filter -// on the immediate parent to make the solid background color -// transparent. - -// See http://github.com/laughinghan/mathquill/wiki/Transforms -// for more details. - -.mq-math-mode { - .mq-matrixed { - background: white; - .inline-block; - } - .mq-matrixed-container { - filter: progid:DXImageTransform.Microsoft.Chroma(color='white'); - margin-top: -.1em; - } -} diff --git a/src/css/selections.less b/src/css/selections.less index 1bf31164b..86459f69c 100644 --- a/src/css/selections.less +++ b/src/css/selections.less @@ -14,41 +14,12 @@ background: #B4D5FE !important; } - .mq-matrixed { - // The Chroma filter doesn't support the 'Highlight' keyword, - // but is only used in IE 8 and below anyway, so just use the - // default Windows highlight color. Even if the highlight color - // of the system has been customized, it's not a big deal, - // most of the solid blue area is chroma keyed, there'll just - // be a blue anti-aliased fringe around the matrix-filter- - // stretched text. - - // If you use IE 8 or below and customized your highlight - // color, and after the effort I put into making everything - // else in MathQuill work in IE 8 and below have the *gall* - // to complain about the blue fringe that appears in selections - // around the otherwise beautifully stretched square roots and - // stuff, and you have no ideas for how to solve the problem, - // just a complaint, then I'd like to politely suggest that you - // go choke on a dick. Unless you're into that, in which case, - // go do something that would make you unhappy instead. - - background: #39F !important; - } - .mq-matrixed-container { - filter: progid:DXImageTransform.Microsoft.Chroma(color='#3399FF') !important; - } - &.mq-blur { - &, & .mq-non-leaf, & .mq-scaled, & .mq-matrixed { + &, & .mq-non-leaf, & .mq-scaled { background: #D4D4D4 !important; color: black; border-color: black; } - - .mq-matrixed-container { - filter: progid:DXImageTransform.Microsoft.Chroma(color='#D4D4D4') !important; - } } } } diff --git a/test/visual.html b/test/visual.html index 2cb3be29e..d10e8b89a 100644 --- a/test/visual.html +++ b/test/visual.html @@ -44,8 +44,7 @@ } /* Non-white background test */ -.different-bgcolor.mq-editable-field, -.different-bgcolor.mq-editable-field .mq-matrixed { +.different-bgcolor.mq-editable-field { background: black; color: white; } From f88527ed185860361bee7368613a002279f905e5 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Fri, 12 Apr 2019 22:57:54 -0400 Subject: [PATCH 287/393] fix selection of nthroot --- src/commands/math/commands.js | 12 +++++++----- src/css/selections.less | 16 ++++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index b888cef13..bbe9ffd0f 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -578,12 +578,14 @@ var Hat = LatexCmds.hat = P(MathCommand, function(_, super_) { var NthRoot = LatexCmds.nthroot = P(SquareRoot, function(_, super_) { _.htmlTemplate = - '&0' - + '' - + '' - + SVG_SYMBOLS.sqrt.html + '' + + '&0' + + '' + + '' + + SVG_SYMBOLS.sqrt.html + + '' + + '&1' + '' - + '&1' + '' ; _.textTemplate = ['sqrt[', '](', ')']; diff --git a/src/css/selections.less b/src/css/selections.less index 86459f69c..eb8702a4c 100644 --- a/src/css/selections.less +++ b/src/css/selections.less @@ -23,3 +23,19 @@ } } } + + +html body { // adding 'html body' for specificity + .mq-math-mode, .mq-editable-field { + .mq-selection { + // do not show a background inside any of the + // children of nthroot. We draw a background on + // the nthroot itself. We don't want the index + // to be covered up by the background of the + // radical. + .mq-nthroot-container * { + background: transparent !important; + } + } + } +} From 9df4cdf81c907f4fdd45cc97ae4ddaab5d0123e9 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Thu, 25 Apr 2019 14:39:59 -0400 Subject: [PATCH 288/393] make cursor color match text color --- src/css/editable.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/css/editable.less b/src/css/editable.less index 59318199c..52e154513 100644 --- a/src/css/editable.less +++ b/src/css/editable.less @@ -1,7 +1,7 @@ .mq-editable-field { .inline-block; .mq-cursor { - border-left: 1px solid black; + border-left: 1px solid currentColor; margin-left: -1px; position: relative; z-index: 1; From d6484e002fc62f31b6e19f65e20ecefc632fc0df Mon Sep 17 00:00:00 2001 From: Eli Luberoff Date: Sat, 4 May 2019 10:41:40 -0700 Subject: [PATCH 289/393] add support for cbrt plus tests --- src/commands/math/commands.js | 9 +++++++++ test/basic.html | 2 +- test/unit/typing.test.js | 6 +++++- 3 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index bbe9ffd0f..244c8ab5e 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -599,6 +599,15 @@ LatexCmds.nthroot = P(SquareRoot, function(_, super_) { }; }); +var CubeRoot = +LatexCmds.cbrt = P(NthRoot, function(_, super_) { + _.createLeftOf = function(cursor) { + super_.createLeftOf.apply(this, arguments); + Digit('3').createLeftOf(cursor); + cursor.controller.moveRight(); + }; +}); + var DiacriticAbove = P(MathCommand, function(_, super_) { _.init = function(ctrlSeq, symbol, textTemplate) { var htmlTemplate = diff --git a/test/basic.html b/test/basic.html index fa1883805..3945e9ef9 100644 --- a/test/basic.html +++ b/test/basic.html @@ -39,7 +39,7 @@

MathQuill-basic Demo local }); var mq = MQ.MathField($('#basic')[0], { autoSubscriptNumerals: true, - autoCommands: 'alpha beta sqrt theta phi pi tau nthroot sum prod int ans percent mid square', + autoCommands: 'alpha beta sqrt theta phi pi tau nthroot cbrt prod int ans percent mid square', autoParenthesizedFunctions: 'sin cos', handlers: { edit: function() { if (!latex.is(':focus')) latex.val(mq.latex()); diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index f203436e0..2822cdc58 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -912,7 +912,7 @@ suite('typing with auto-replaces', function() { setup(function() { mq.config({ autoOperatorNames: 'sin pp', - autoCommands: 'pi tau phi theta Gamma sum prod sqrt nthroot percent' + autoCommands: 'pi tau phi theta Gamma sum prod sqrt nthroot cbrt percent' }); }); @@ -962,6 +962,10 @@ suite('typing with auto-replaces', function() { assertLatex('\\%\\operatorname{of}'); mq.keystroke('Backspace'); + mq.typedText('cbrt'); + assertLatex('\\sqrt[3]{}'); + mq.typedText('pi'); + assertLatex('\\sqrt[3]{\\pi}'); }); test('sequences of auto-commands and other assorted characters', function() { From 2beb4a0888f53e0868251baca8e1a76b919c163d Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Thu, 9 May 2019 11:00:37 -0400 Subject: [PATCH 290/393] Adjust how Mathquill speaks nthroots Instead of 'Start 3 root, 5' for cube root of 5, now speaks 'Root Index 3, start root, 5, end root.' --- src/commands/math/commands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 244c8ab5e..c4e729b22 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -595,7 +595,7 @@ LatexCmds.nthroot = P(SquareRoot, function(_, super_) { _.mathspeak = function() { this.ends[L].ariaLabel = 'Index'; this.ends[R].ariaLabel = 'Radicand'; - return 'Start '+this.ends[L].mathspeak()+' Root, '+this.ends[R].mathspeak()+', End Root'; + return 'Root Index '+this.ends[L].mathspeak()+', Start Root, '+this.ends[R].mathspeak()+', End Root'; }; }); From c9bf6d34612330fdaff2e40514d4852929e253f0 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Thu, 9 May 2019 11:04:27 -0400 Subject: [PATCH 291/393] Update nthroot mathspeak test --- test/unit/typing.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index 2822cdc58..0ceecc850 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -935,7 +935,7 @@ suite('typing with auto-replaces', function() { mq.typedText('nthroot'); mq.typedText('n').keystroke('Right').typedText('100').keystroke('Right'); assertLatex('\\sqrt[n]{100}'); - assertMathspeak('Start "n" Root 100 End Root'); + assertMathspeak('Root Index "n" Start Root 100 End Root'); mq.keystroke('Ctrl-Backspace'); mq.typedText('pi'); From 7bb1712ca70e4c550d25aa5ebb39e77b59038aa6 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Thu, 9 May 2019 12:21:58 -0400 Subject: [PATCH 292/393] Add mathspeak exception for voicing cube roots --- src/commands/math/commands.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index c4e729b22..b461c7a55 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -593,9 +593,15 @@ LatexCmds.nthroot = P(SquareRoot, function(_, super_) { return '\\sqrt['+this.ends[L].latex()+']{'+this.ends[R].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'; - return 'Root Index '+this.ends[L].mathspeak()+', Start Root, '+this.ends[R].mathspeak()+', End Root'; + if (indexMathspeak === '3') { // cube root + return 'Start Cube Root, '+radicandMathspeak+', End Cube Root'; + } else { + return 'Root Index '+indexMathspeak+', Start Root, '+radicandMathspeak+', End Root'; + } }; }); From 3dc3bf81a38e5cfedc53ce540223a5281448ed6d Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Thu, 9 May 2019 12:26:40 -0400 Subject: [PATCH 293/393] Add cube root mathspeak assertion --- test/unit/typing.test.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index 0ceecc850..de0c03e14 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -964,6 +964,7 @@ suite('typing with auto-replaces', function() { mq.typedText('cbrt'); assertLatex('\\sqrt[3]{}'); + assertMathspeak('Start Cube Root End Cube Root'); mq.typedText('pi'); assertLatex('\\sqrt[3]{\\pi}'); }); From 43523bee163d97d2df1ee590f76b3d1810b34f7a Mon Sep 17 00:00:00 2001 From: Eli Luberoff Date: Wed, 15 May 2019 07:28:49 -0700 Subject: [PATCH 294/393] inherit color for underline/overline fixes display with any color except black --- src/css/math.less | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/css/math.less b/src/css/math.less index 0422925f4..a11824f78 100644 --- a/src/css/math.less +++ b/src/css/math.less @@ -180,11 +180,11 @@ } .mq-overline { - border-top: 1px solid black; + border-top: 1px solid; margin-top: 1px; } .mq-underline { - border-bottom: 1px solid black; + border-bottom: 1px solid; margin-bottom: 1px; } @@ -406,14 +406,14 @@ } .mq-overarc { - border-top: 1px solid black; + border-top: 1px solid; border-radius: 50% 50% 0 0; padding: 0.35em 0.25em 0 0.1em; margin-top: 0.1em; } .mq-overarrow { - border-top: 1px solid black; + border-top: 1px solid; margin-top: 1px; padding-top: 0.2em; position: relative; From af9b1751487724fff8b031569b8f2b0df4c61317 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Thu, 30 May 2019 14:05:33 -0400 Subject: [PATCH 295/393] do not unitalice autoOperatorNames when in simple subscript --- src/commands/math/basicSymbols.js | 11 +++++++++++ src/tree.js | 7 +++++++ 2 files changed, 18 insertions(+) diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index 44a54fbbb..808c4137b 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -296,6 +296,11 @@ var Letter = P(Variable, function(_, super_) { return } + //exit early if in simple subscript + if (this.isParentSimpleSubscript()) { + return; + } + //handle autoParenthesized functions var str = '', l = this, i = 0; @@ -335,6 +340,12 @@ var Letter = P(Variable, function(_, super_) { _.autoUnItalicize = function(opts) { var autoOps = opts.autoOperatorNames; if (autoOps._maxLength === 0) return; + + //exit early if in simple subscript + if (this.isParentSimpleSubscript()) { + return; + } + // want longest possible operator names, so join together entire contiguous // sequence of letters var str = this.letter; diff --git a/src/tree.js b/src/tree.js index 2712eda35..606a6e0fc 100644 --- a/src/tree.js +++ b/src/tree.js @@ -302,6 +302,13 @@ var Node = P(function(_) { return this.disown(); }; + _.isParentSimpleSubscript = function () { + if (!this.parent) return false; + if (!this.parent.jQ.hasClass('mq-sub')) return false; + if (!(this.parent.parent instanceof SupSub)) return false; + return true; + }; + // Overridden by child classes _.finalizeTree = function () { }; _.contactWeld = function () { }; From b90877d0275f5deebef2352bc6270535d6f75db1 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Thu, 30 May 2019 16:40:40 -0400 Subject: [PATCH 296/393] add some tests --- test/unit/autoOperatorNames.test.js | 25 +++++++++++++++++++++++++ test/unit/typing.test.js | 27 ++++++++++++++++++++++++++- 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/test/unit/autoOperatorNames.test.js b/test/unit/autoOperatorNames.test.js index 25d5caccd..c8d5eb86e 100644 --- a/test/unit/autoOperatorNames.test.js +++ b/test/unit/autoOperatorNames.test.js @@ -2,6 +2,9 @@ suite('autoOperatorNames', function() { var mq; setup(function() { mq = MQ.MathField($('').appendTo('#mock')[0]); + mq.config({ + autoCommands: 'sum int' + }); }); function assertLatex(input, expected) { @@ -51,6 +54,28 @@ suite('autoOperatorNames', function() { assertAutoOperatorNamesWork('scscscscscsc', 's\\csc s\\csc s\\csc'); }); + test('works in \\sum', function () { + //autoParenthesized and also operatored + mq.typedText('sum') + mq.typedText('sin') + assertLatex('sum allows operatorname', '\\sum_{\\sin}^{ }'); + }) + + test('works in \\int', function () { + //autoParenthesized and also operatored + mq.typedText('int') + mq.typedText('sin') + assertLatex('int allows operatorname', '\\int_{\\sin}^{ }'); + }) + + test('does not work in simple subscripts', function () { + //autoParenthesized and also operatored + mq.typedText('x_') + mq.typedText('sin') + assertLatex('subscripts do not turn to operatorname','x_{sin}'); + }) + + test('text() output', function(){ function assertTranslatedCorrectly(latexStr, text) { mq.latex(latexStr); diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index de0c03e14..37b841005 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -851,7 +851,8 @@ suite('typing with auto-replaces', function() { setup(function() { mq.config({ autoParenthesizedFunctions: 'sin cos tan ln', - autoOperatorNames: 'sin ln' + autoOperatorNames: 'sin ln', + autoCommands: 'sum int' }); }); @@ -892,6 +893,30 @@ suite('typing with auto-replaces', function() { assertLatex('\\sin\\left(\\right)'); }) + test('works in \\sum', function () { + //autoParenthesized and also operatored + mq.typedText('sum') + assertLatex('\\sum_{ }^{ }'); + mq.typedText('sin') + assertLatex('\\sum_{\\sin\\left(\\right)}^{ }'); + }) + + test('works in \\int', function () { + //autoParenthesized and also operatored + mq.typedText('int') + assertLatex('\\int_{ }^{ }'); + mq.typedText('sin') + assertLatex('\\int_{\\sin\\left(\\right)}^{ }'); + }) + + test('does not work in simple subscripts', function () { + //autoParenthesized and also operatored + mq.typedText('x_') + assertLatex('x_{ }'); + mq.typedText('sin') + assertLatex('x_{sin}'); + }) + }); suite('typingSlashCreatesNewFraction', function() { From 619bb2301d04ef60d16740a27e641e2b074cfa48 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Thu, 30 May 2019 21:24:26 -0400 Subject: [PATCH 297/393] remove jQ check --- src/tree.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/tree.js b/src/tree.js index 606a6e0fc..20dea5c65 100644 --- a/src/tree.js +++ b/src/tree.js @@ -304,8 +304,8 @@ var Node = P(function(_) { _.isParentSimpleSubscript = function () { if (!this.parent) return false; - if (!this.parent.jQ.hasClass('mq-sub')) return false; if (!(this.parent.parent instanceof SupSub)) return false; + if (this.parent.parent.sub !== this.parent) return false; return true; }; From 03a970036afd9db1b6454029fb7aa63406e9539a Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Fri, 31 May 2019 21:15:44 -0400 Subject: [PATCH 298/393] make sure operatornames do not expand in subscripts when pasting --- src/tree.js | 13 ++++++++++++- test/unit/autoOperatorNames.test.js | 9 +++++---- test/unit/typing.test.js | 7 ++++--- 3 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/tree.js b/src/tree.js index 20dea5c65..9a06be21f 100644 --- a/src/tree.js +++ b/src/tree.js @@ -305,7 +305,18 @@ var Node = P(function(_) { _.isParentSimpleSubscript = function () { if (!this.parent) return false; if (!(this.parent.parent instanceof SupSub)) return false; - if (this.parent.parent.sub !== this.parent) return false; + + // Mathquill is gross. There are many different paths that + // create subscripts and sometimes we don't even construct + // true instances of `LatexCmds._`. Another problem is that + // the relationship between the sub and the SupSub isn't + // completely setup during a paste at the time we check + // this. I wanted to use: `this.parent.parent.sub !== this.parent` + // but that check doesn't always work. This seems to be the only + // check that always works. I'd rather live with this than try + // to change the init order of things. + if (!this.parent.jQ.hasClass('mq-sub')) return false; + return true; }; diff --git a/test/unit/autoOperatorNames.test.js b/test/unit/autoOperatorNames.test.js index c8d5eb86e..f68aeebce 100644 --- a/test/unit/autoOperatorNames.test.js +++ b/test/unit/autoOperatorNames.test.js @@ -55,26 +55,27 @@ suite('autoOperatorNames', function() { }); test('works in \\sum', function () { - //autoParenthesized and also operatored mq.typedText('sum') mq.typedText('sin') assertLatex('sum allows operatorname', '\\sum_{\\sin}^{ }'); }) test('works in \\int', function () { - //autoParenthesized and also operatored mq.typedText('int') mq.typedText('sin') assertLatex('int allows operatorname', '\\int_{\\sin}^{ }'); }) - test('does not work in simple subscripts', function () { - //autoParenthesized and also operatored + test('does not work in simple subscripts when typing', function () { mq.typedText('x_') mq.typedText('sin') assertLatex('subscripts do not turn to operatorname','x_{sin}'); }) + test('does not work in simple subscripts when pasting', function () { + $(mq.el()).find('textarea').trigger('paste').val('x_{sin}').trigger('input'); + assertLatex('subscripts do not turn to operatorname','x_{sin}'); + }) test('text() output', function(){ function assertTranslatedCorrectly(latexStr, text) { diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index 37b841005..1b874f196 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -894,7 +894,6 @@ suite('typing with auto-replaces', function() { }) test('works in \\sum', function () { - //autoParenthesized and also operatored mq.typedText('sum') assertLatex('\\sum_{ }^{ }'); mq.typedText('sin') @@ -902,7 +901,6 @@ suite('typing with auto-replaces', function() { }) test('works in \\int', function () { - //autoParenthesized and also operatored mq.typedText('int') assertLatex('\\int_{ }^{ }'); mq.typedText('sin') @@ -910,13 +908,16 @@ suite('typing with auto-replaces', function() { }) test('does not work in simple subscripts', function () { - //autoParenthesized and also operatored mq.typedText('x_') assertLatex('x_{ }'); mq.typedText('sin') assertLatex('x_{sin}'); }) + test('does not work in simple subscripts when pasting', function () { + $(mq.el()).find('textarea').trigger('paste').val('x_{sin}').trigger('input'); + assertLatex('x_{sin}'); + }) }); suite('typingSlashCreatesNewFraction', function() { From 9007e1848933970ecff972fbb4f2d4a2665b5746 Mon Sep 17 00:00:00 2001 From: Eli Luberoff Date: Tue, 7 May 2019 19:04:17 -0700 Subject: [PATCH 299/393] Add support for approx Behavior matches inequalities, with typing two twiddles making the double twiddle and backspace going back to single twiddle --- src/commands/math/basicSymbols.js | 54 +++++++++++++++++++++++++++++-- 1 file changed, 52 insertions(+), 2 deletions(-) diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index 808c4137b..c2e9fcfd5 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -843,7 +843,6 @@ LatexCmds.infty = LatexCmds.infin = LatexCmds.infinity = bind(VanillaSymbol,'\\infty ','∞', 'infinity'); LatexCmds['≠'] = LatexCmds.ne = LatexCmds.neq = bind(BinaryOperator,'\\ne ','≠', 'not equal'); - var Equality = P(BinaryOperator, function(_, super_) { _.init = function() { super_.init.call(this, '=', '=', '=', 'equals'); @@ -864,4 +863,55 @@ LatexCmds['×'] = LatexCmds.times = bind(BinaryOperator, '\\times ', '×', LatexCmds['÷'] = LatexCmds.div = LatexCmds.divide = LatexCmds.divides = bind(BinaryOperator,'\\div ','÷', '[/]', 'over'); -CharCmds['~'] = LatexCmds.sim = bind(BinaryOperator, '\\sim ', '~', '~', 'tilde'); + +var twiddleData = { + ctrlSeq: { + single: '\\sim', + double: '\\approx' + }, + html: { + single: '~', + double: '≈' + }, + text: { + single: '~', + double: '≈' + }, + mathspeak: { + single: 'tilde', + double: 'approximately equal' + } +} + +var Twiddle = P(BinaryOperator, function(_, super_) { + _.init = function(type) { /* 'single' | 'double' */ + this.type = type; + super_.init.call(this, twiddleData.ctrlSeq[type], twiddleData.html[type], twiddleData.text[type], twiddleData.mathspeak[type]); + }; + _.createLeftOf = function(cursor) { + if (cursor[L] instanceof Twiddle) { + cursor[L].swap('double'); + cursor[L].bubble(function (node) { node.reflow(); }); + return; + } + super_.createLeftOf.apply(this, arguments); + }; + _.swap = function(type) { + this.type = type; + this.ctrlSeq = twiddleData.ctrlSeq[type]; + this.jQ.html(twiddleData.html[type]); + this.textTemplate = [ twiddleData.text[type] ]; + this.mathspeakName = twiddleData.mathspeak[type]; + }; + _.deleteTowards = function(dir, cursor) { + if (dir === L && this.type === 'double') { + this.swap('single'); + this.bubble(function (node) { node.reflow(); }); + return; + } + super_.deleteTowards.apply(this, arguments); + }; +}); + +CharCmds['~'] = LatexCmds.sim = bind(Twiddle, 'single'); +LatexCmds['≈'] = LatexCmds.approx = bind(Twiddle, 'double'); From 6d56aa7808b6885e56e7d69eb23a56840a4eb61c Mon Sep 17 00:00:00 2001 From: Eli Luberoff Date: Thu, 9 May 2019 17:33:27 -0700 Subject: [PATCH 300/393] add tests for typing and editing approx --- test/unit/typing.test.js | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index 1b874f196..6ab06437e 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -1103,7 +1103,7 @@ suite('typing with auto-replaces', function() { assertLatex('<<>\\ge=>><\\le='); assertMathspeak('less than less than greater than greater than or equal to equals greater than greater than less than less than or equal to equals'); }); - + test('typing ≤ and ≥ chars directly', function() { mq.typedText('≤'); assertFullyFunctioningInequality('\\le', '<', 'less than or equal to', 'less than'); @@ -1112,6 +1112,26 @@ suite('typing with auto-replaces', function() { assertFullyFunctioningInequality('\\ge', '>', 'greater than or equal to', 'greater than'); }); + test('typing and backspacing ~', function() { + mq.typedText('~'); + assertLatex('\\sim'); + assertMathspeak('tilde'); + mq.typedText('~'); + assertLatex('\\approx'); + assertMathspeak('approximately equal'); + mq.keystroke('Backspace'); + assertLatex('\\sim'); + assertMathspeak('tilde'); + }); + test('typing ≈ char directly', function() { + mq.typedText('≈'); + assertLatex('\\approx'); + assertMathspeak('approximately equal'); + mq.keystroke('Backspace'); + assertLatex('\\sim'); + assertMathspeak('tilde'); + }); + suite('rendered from LaTeX', function() { test('control sequences', function() { mq.latex('\\le'); From 8ad294db085ba299618ed7e09ef08c932be118e7 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Thu, 13 Jun 2019 12:36:39 -0400 Subject: [PATCH 301/393] Refactor sim and approx rendering based on code review comments --- src/commands/math/basicSymbols.js | 57 ++++++++++++------------------- 1 file changed, 21 insertions(+), 36 deletions(-) diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index c2e9fcfd5..403ca708c 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -864,54 +864,39 @@ LatexCmds['÷'] = LatexCmds.div = LatexCmds.divide = LatexCmds.divides = bind(BinaryOperator,'\\div ','÷', '[/]', 'over'); -var twiddleData = { - ctrlSeq: { - single: '\\sim', - double: '\\approx' - }, - html: { - single: '~', - double: '≈' - }, - text: { - single: '~', - double: '≈' - }, - mathspeak: { - single: 'tilde', - double: 'approximately equal' - } -} - -var Twiddle = P(BinaryOperator, function(_, super_) { - _.init = function(type) { /* 'single' | 'double' */ - this.type = type; - super_.init.call(this, twiddleData.ctrlSeq[type], twiddleData.html[type], twiddleData.text[type], twiddleData.mathspeak[type]); +var Sim = P(BinaryOperator, function(_, super_) { + _.init = function() { + super_.init.call(this, '\\sim', '~', '~', 'tilde'); }; _.createLeftOf = function(cursor) { - if (cursor[L] instanceof Twiddle) { - cursor[L].swap('double'); + if (cursor[L] instanceof Sim) { + var l = cursor[L]; + cursor[L] = l[L]; + l.remove(); + Approx().createLeftOf(cursor); cursor[L].bubble(function (node) { node.reflow(); }); return; } super_.createLeftOf.apply(this, arguments); }; - _.swap = function(type) { - this.type = type; - this.ctrlSeq = twiddleData.ctrlSeq[type]; - this.jQ.html(twiddleData.html[type]); - this.textTemplate = [ twiddleData.text[type] ]; - this.mathspeakName = twiddleData.mathspeak[type]; +}); + +var Approx = P(BinaryOperator, function(_, super_) { + _.init = function() { + super_.init.call(this, '\\approx', '≈', '≈', 'approximately equal'); }; _.deleteTowards = function(dir, cursor) { - if (dir === L && this.type === 'double') { - this.swap('single'); - this.bubble(function (node) { node.reflow(); }); + if (dir === L) { + var l = cursor[L]; + Fragment(l, this).remove(); + cursor[L] = l[L]; + Sim().createLeftOf(cursor); + cursor[L].bubble(function (node) { node.reflow(); }); return; } super_.deleteTowards.apply(this, arguments); }; }); -CharCmds['~'] = LatexCmds.sim = bind(Twiddle, 'single'); -LatexCmds['≈'] = LatexCmds.approx = bind(Twiddle, 'double'); +CharCmds['~'] = LatexCmds.sim = Sim; +LatexCmds['≈'] = LatexCmds.approx = Approx; From ce9996b139f5264d54115cf0d2ade0c52c01a53b Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Fri, 14 Jun 2019 15:17:36 -0400 Subject: [PATCH 302/393] Add more sim and approx typing/backspace tests --- test/unit/typing.test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index 6ab06437e..715b996ea 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -1119,6 +1119,18 @@ suite('typing with auto-replaces', function() { mq.typedText('~'); assertLatex('\\approx'); assertMathspeak('approximately equal'); + mq.typedText('~'); + assertLatex('\\approx\\sim'); + assertMathspeak('approximately equal tilde'); + mq.typedText('~'); + assertLatex('\\approx\\approx'); + assertMathspeak('approximately equal approximately equal'); + mq.keystroke('Backspace'); + assertLatex('\\approx\\sim'); + assertMathspeak('approximately equal tilde'); + mq.keystroke('Backspace'); + assertLatex('\\approx'); + assertMathspeak('approximately equal'); mq.keystroke('Backspace'); assertLatex('\\sim'); assertMathspeak('tilde'); From 84b092370813343ec028756be92449657b033c03 Mon Sep 17 00:00:00 2001 From: Jason Merrill Date: Thu, 20 Jun 2019 16:27:38 -0700 Subject: [PATCH 303/393] Fix serializing \sim and \approx. When we serialize to LaTeX, commands like this need to have a space after them to separate them from following letters and numbers. --- src/commands/math/basicSymbols.js | 4 ++-- test/unit/typing.test.js | 10 +++++++++- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index 403ca708c..43fba2f88 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -866,7 +866,7 @@ LatexCmds['÷'] = LatexCmds.div = LatexCmds.divide = LatexCmds.divides = var Sim = P(BinaryOperator, function(_, super_) { _.init = function() { - super_.init.call(this, '\\sim', '~', '~', 'tilde'); + super_.init.call(this, '\\sim ', '~', '~', 'tilde'); }; _.createLeftOf = function(cursor) { if (cursor[L] instanceof Sim) { @@ -883,7 +883,7 @@ var Sim = P(BinaryOperator, function(_, super_) { var Approx = P(BinaryOperator, function(_, super_) { _.init = function() { - super_.init.call(this, '\\approx', '≈', '≈', 'approximately equal'); + super_.init.call(this, '\\approx ', '≈', '≈', 'approximately equal'); }; _.deleteTowards = function(dir, cursor) { if (dir === L) { diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index 715b996ea..a22bb62d2 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -1103,7 +1103,7 @@ suite('typing with auto-replaces', function() { assertLatex('<<>\\ge=>><\\le='); assertMathspeak('less than less than greater than greater than or equal to equals greater than greater than less than less than or equal to equals'); }); - + test('typing ≤ and ≥ chars directly', function() { mq.typedText('≤'); assertFullyFunctioningInequality('\\le', '<', 'less than or equal to', 'less than'); @@ -1134,6 +1134,14 @@ suite('typing with auto-replaces', function() { mq.keystroke('Backspace'); assertLatex('\\sim'); assertMathspeak('tilde'); + mq.keystroke('Backspace'); + mq.typedText('a~b'); + assertLatex('a\\sim b'); + assertMathspeak('"a" tilde "b"'); + mq.keystroke('Backspace'); + mq.typedText('~b'); + assertLatex('a\\approx b'); + assertMathspeak('"a" approximately equal "b"'); }); test('typing ≈ char directly', function() { mq.typedText('≈'); From fc5c7e0c4a995aae0fc959cf4ff92b74e10a1853 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Mon, 1 Jul 2019 21:50:05 -0400 Subject: [PATCH 304/393] allow pasting in the sqrt symbol --- src/commands/math/basicSymbols.js | 24 ++++++++++++++++++++++++ src/commands/math/commands.js | 3 +-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index 43fba2f88..14de8316f 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -763,6 +763,30 @@ LatexCmds['¼'] = bind(LatexFragment, '\\frac14'); LatexCmds['½'] = bind(LatexFragment, '\\frac12'); LatexCmds['¾'] = bind(LatexFragment, '\\frac34'); +// this is a hack to make pasting the √ symbol +// actually insert a sqrt command. This isn't ideal, +// but it's way better than what we have now. I think +// before we invest any more time into this single character +// we should consider how to make the pipe (|) automatically +// insert absolute value. We also will want the percent (%) +// to expand to '% of'. I've always just thought mathquill's +// ability to handle pasted latex magical until I started actually +// testing it. It's a lot more buggy that I previously thought. +// +// KNOWN ISSUES: +// 1) pasting √ does not put focus in side the sqrt symbol +// 2) pasting √2 puts the 2 outside of the sqrt symbol. +// +// The first issue seems like we could invest more time into this to +// fix it, but doesn't feel worth special casing. I think we'd want +// to address it by addressing ALL pasting issues. +// +// The second issue seems like it might go away too if you fix paste to +// 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{}'); + var PlusMinus = P(BinaryOperator, function(_) { _.init = VanillaSymbol.prototype.init; diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index b461c7a55..cba39f42f 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -537,8 +537,7 @@ LatexCmds.percentof = P(Symbol, function (_, super_) { }); var SquareRoot = -LatexCmds.sqrt = -LatexCmds['√'] = P(MathCommand, function(_, super_) { +LatexCmds.sqrt = P(MathCommand, function(_, super_) { _.ctrlSeq = '\\sqrt'; _.htmlTemplate = '' From d8482a74588439607efa2e38cdc852c376eb4525 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Wed, 3 Jul 2019 01:48:49 -0400 Subject: [PATCH 305/393] fix layout of integrals within exponents --- src/css/math.less | 2 +- test/unit/updown.test.js | 21 +++++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/css/math.less b/src/css/math.less index a11824f78..81b983a83 100644 --- a/src/css/math.less +++ b/src/css/math.less @@ -213,7 +213,7 @@ &.mq-sup-only { vertical-align: .5em; - .mq-sup { + & > .mq-sup { display: inline-block; vertical-align: text-bottom; } diff --git a/test/unit/updown.test.js b/test/unit/updown.test.js index 783036a0b..52fe6ce08 100644 --- a/test/unit/updown.test.js +++ b/test/unit/updown.test.js @@ -171,6 +171,27 @@ suite('up/down', function() { assert.equal(cursor[L], sub, 'cursor up up from subscript fraction denominator that is at right end goes after subscript'); }); + test('integral in exponent', function () { + controller.renderLatexMath('2^{\\int_0^1}'); + var exp = rootBlock.ends[R], + expBlock = exp.ends[L]; + + mq.keystroke('Up'); + mq.keystroke('Up'); + assert.equal(cursor.parent.latex(), '1', 'cursor up goes to upper limit'); + var upperRect = cursor.parent.jQ[0].getBoundingClientRect(); + + mq.keystroke('Down'); + assert.equal(cursor.parent.latex(), '0', 'cursor down goes to lower limit'); + var lowerRect = cursor.parent.jQ[0].getBoundingClientRect(); + + mq.keystroke('Up'); + assert.equal(cursor.parent.latex(), '1', 'cursor up goes to upper limit'); + + var upperAboveLower = upperRect.bottom < lowerRect.top; + assert.equal(upperAboveLower, true, 'cursor actually moves downward for lower limit'); + }); + test('\\MathQuillMathField{} in a fraction', function() { var outer = MQ.StaticMath( $('\\frac{\\MathQuillMathField{n}}{2}').appendTo('#mock')[0] From 6f77d6c4e9aa6975cbf77d6376d82d73daa54d60 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Fri, 28 Jun 2019 11:19:19 -0400 Subject: [PATCH 306/393] Make test server accurately report host and port on which it's listening --- script/test_server.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/script/test_server.js b/script/test_server.js index 17a7420d9..b653eb424 100644 --- a/script/test_server.js +++ b/script/test_server.js @@ -84,8 +84,9 @@ function run_make_test() { if (code) { console.error('Exit Code ' + code); } else { - console.log('\nMathQuill is now running on localhost:9292'); - console.log('Open http://localhost:9292/test/demo.html\n'); + var serverAddress = HOST === '0.0.0.0' ? 'localhost:' + PORT : HOST + ':' + PORT; + console.log('\nMathQuill is now running on ' + serverAddress); + console.log('Open http://' + serverAddress + '/test/demo.html\n'); } for (var i = 0; i < q.length; i += 1) q[i](); q = undefined; From 5fca8753403a108f5f51d0a1c61584ea833b6af7 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 9 Jul 2019 11:13:55 -0400 Subject: [PATCH 307/393] Modify unit test page to synthesize results in a form our automated testing framework understands This also removes the xunit support (still in mainline) because we don't use it. --- test/unit.html | 146 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 119 insertions(+), 27 deletions(-) diff --git a/test/unit.html b/test/unit.html index 936982410..2062b8b0e 100644 --- a/test/unit.html +++ b/test/unit.html @@ -24,15 +24,12 @@ @@ -54,35 +51,130 @@

Unit Tests

+
From e116fabe1370f84315a16133585059b382266827 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 9 Jul 2019 17:13:00 -0400 Subject: [PATCH 308/393] Rework suite and test assertion tracking --- test/unit.html | 52 +++++++++++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/test/unit.html b/test/unit.html index 2062b8b0e..e0fd9c2e3 100644 --- a/test/unit.html +++ b/test/unit.html @@ -26,9 +26,8 @@ @@ -58,18 +57,17 @@

Unit Tests

var json = location.search.indexOf('json') >= 0; var listTests = location.search.indexOf('listTests') >= 0; var startTime = Date.now(); - var moduleResults = []; - var currentSuite; + var suiteMap = {}; var runner = mocha.run(); runner.on('suite', function(suite) { - var suiteResults = {}; - suiteResults.name = xmlEscape(suite.title); - suiteResults.time = Date.now() - startTime; - suiteResults.assertions = []; - currentSuite = suiteResults; + var title = xmlEscape(suite.title); + suiteMap[title] = { + time: Date.now(), + assertions: [] + }; if (listTests) { - currentSuite.assertions.push({ + suiteMap[title].assertions.push({ elapsedTime: 0, timestamp: 0, result: true, @@ -78,18 +76,12 @@

Unit Tests

} }); - runner.on('suite end', function() { - var lastPushedSuite = moduleResults[moduleResults.length - 1]; - if (!lastPushedSuite || lastPushedSuite.name !== currentSuite.name) { - moduleResults.push(currentSuite); - } - }); - runner.on('pass', function(test) { if (!listTests) { + var title = getTestSuiteTitle(test); var elapsedTime = Date.now() - startTime; - var timestamp = elapsedTime - currentSuite.time; - currentSuite.assertions.push({ + var timestamp = Date.now(); + suiteMap[title].assertions.push({ elapsedTime: elapsedTime, timestamp: timestamp, result: true, @@ -100,9 +92,10 @@

Unit Tests

runner.on('fail', function(test, err) { if (!listTests) { + var title = getTestSuiteTitle(test); var elapsedTime = Date.now() - startTime; - var timestamp = elapsedTime - currentSuite.time; - currentSuite.assertions.push({ + var timestamp = Date.now(); + suiteMap[title].assertions.push({ elapsedTime: elapsedTime, timestamp: timestamp, result: false, @@ -115,6 +108,17 @@

Unit Tests

}); runner.on('end', function() { + var moduleResults = []; + for (var suiteTitle in suiteMap) { + if (suiteMap.hasOwnProperty(suiteTitle)) { + var suiteResults = suiteMap[suiteTitle]; + moduleResults.push({ + name: suiteTitle, + time: suiteResults.time, + assertions: suiteResults.assertions + }); + } + } var testResults = { modules: { "mathquill": moduleResults }, passes: runner.stats.passes, @@ -128,6 +132,10 @@

Unit Tests

} }); + function getTestSuiteTitle(test) { + return xmlEscape(test.parent.title); + } + // must escape a few symbols in xml attributes: //http://stackoverflow.com/questions/866706/which-characters-are-invalid-unless-encoded-in-an-xml-attribute function xmlEscape(string){ From 2df74fa1238c42a55ab923c36199fb6b710d2f0c Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 9 Jul 2019 17:27:12 -0400 Subject: [PATCH 309/393] Use suite full title when mapping tests --- test/unit.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit.html b/test/unit.html index e0fd9c2e3..b22b7a6eb 100644 --- a/test/unit.html +++ b/test/unit.html @@ -61,7 +61,7 @@

Unit Tests

var runner = mocha.run(); runner.on('suite', function(suite) { - var title = xmlEscape(suite.title); + var title = xmlEscape(suite.fullTitle()); suiteMap[title] = { time: Date.now(), assertions: [] @@ -133,7 +133,7 @@

Unit Tests

}); function getTestSuiteTitle(test) { - return xmlEscape(test.parent.title); + return xmlEscape(test.parent.fullTitle()); } // must escape a few symbols in xml attributes: From ed5e24303efc8b1820b0b7e65820f15bbfb8e0f1 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 9 Jul 2019 17:41:47 -0400 Subject: [PATCH 310/393] Report elapsed time in seconds --- test/unit.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit.html b/test/unit.html index b22b7a6eb..4d5c8f8c0 100644 --- a/test/unit.html +++ b/test/unit.html @@ -79,7 +79,7 @@

Unit Tests

runner.on('pass', function(test) { if (!listTests) { var title = getTestSuiteTitle(test); - var elapsedTime = Date.now() - startTime; + var elapsedTime = (Date.now() - startTime) / 1000; var timestamp = Date.now(); suiteMap[title].assertions.push({ elapsedTime: elapsedTime, @@ -93,7 +93,7 @@

Unit Tests

runner.on('fail', function(test, err) { if (!listTests) { var title = getTestSuiteTitle(test); - var elapsedTime = Date.now() - startTime; + var elapsedTime = (Date.now() - startTime) / 1000; var timestamp = Date.now(); suiteMap[title].assertions.push({ elapsedTime: elapsedTime, From 1505288fb99daef691b313a015db44bfd48536cf Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 9 Jul 2019 17:48:55 -0400 Subject: [PATCH 311/393] Put suite time in seconds --- test/unit.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit.html b/test/unit.html index 4d5c8f8c0..bfd5c6d8a 100644 --- a/test/unit.html +++ b/test/unit.html @@ -63,7 +63,7 @@

Unit Tests

runner.on('suite', function(suite) { var title = xmlEscape(suite.fullTitle()); suiteMap[title] = { - time: Date.now(), + time: (Date.now() - startTime) / 1000, assertions: [] }; if (listTests) { From 94317a9a7ea3efa456ba6b95e3a61d1af7c8baa0 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 9 Jul 2019 18:20:42 -0400 Subject: [PATCH 312/393] Use test duration when reporting elapsed time and rm suite time property --- test/unit.html | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/test/unit.html b/test/unit.html index bfd5c6d8a..c1fba135d 100644 --- a/test/unit.html +++ b/test/unit.html @@ -63,7 +63,6 @@

Unit Tests

runner.on('suite', function(suite) { var title = xmlEscape(suite.fullTitle()); suiteMap[title] = { - time: (Date.now() - startTime) / 1000, assertions: [] }; if (listTests) { @@ -79,7 +78,7 @@

Unit Tests

runner.on('pass', function(test) { if (!listTests) { var title = getTestSuiteTitle(test); - var elapsedTime = (Date.now() - startTime) / 1000; + var elapsedTime = test.duration / 1000; var timestamp = Date.now(); suiteMap[title].assertions.push({ elapsedTime: elapsedTime, @@ -93,7 +92,7 @@

Unit Tests

runner.on('fail', function(test, err) { if (!listTests) { var title = getTestSuiteTitle(test); - var elapsedTime = (Date.now() - startTime) / 1000; + var elapsedTime = test.duration / 1000; var timestamp = Date.now(); suiteMap[title].assertions.push({ elapsedTime: elapsedTime, @@ -114,7 +113,6 @@

Unit Tests

var suiteResults = suiteMap[suiteTitle]; moduleResults.push({ name: suiteTitle, - time: suiteResults.time, assertions: suiteResults.assertions }); } From d4bbb1c7f65df48b7c3b2a5035eaa0b837810487 Mon Sep 17 00:00:00 2001 From: mikehaverstock Date: Tue, 16 Jul 2019 00:45:54 -0400 Subject: [PATCH 313/393] add sqrt paste tests --- test/unit/paste.test.js | 70 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 test/unit/paste.test.js diff --git a/test/unit/paste.test.js b/test/unit/paste.test.js new file mode 100644 index 000000000..62a37fb98 --- /dev/null +++ b/test/unit/paste.test.js @@ -0,0 +1,70 @@ +suite('paste', function() { + var mq; + setup(function() { + mq = MQ.MathField($('').appendTo('#mock')[0]); + }); + + function prayWellFormedPoint(pt) { prayWellFormed(pt.parent, pt[L], pt[R]); } + + function assertLatex(latex) { + prayWellFormedPoint(mq.__controller.cursor); + assert.equal(mq.latex(), latex); + } + + suite('√', function() { + test('sqrt symbol in empty latex', function() { + $(mq.el()).find('textarea').trigger('paste').val('√').trigger('input'); + assertLatex('\\sqrt{ }'); + }) + test('sqrt symbol in non-empty latex', function() { + mq.latex('1+'); + $(mq.el()).find('textarea').trigger('paste').val('√').trigger('input'); + assertLatex('1+\\sqrt{ }'); + }) + test('sqrt symbol at start of non-empty latex', function () { + mq.latex('1+'); + mq.moveToLeftEnd() + $(mq.el()).find('textarea').trigger('paste').val('√').trigger('input'); + assertLatex('\\sqrt{ }1+'); + }) + }); + + suite('√2', function() { + test('sqrt symbol in empty latex', function() { + $(mq.el()).find('textarea').trigger('paste').val('√2').trigger('input'); + assertLatex('\\sqrt{ }2'); + }) + test('sqrt symbol in non-empty latex', function() { + mq.latex('1+'); + $(mq.el()).find('textarea').trigger('paste').val('√2').trigger('input'); + assertLatex('1+\\sqrt{ }2'); + }) + test('sqrt symbol at start of non-empty latex', function () { + mq.latex('1+'); + mq.moveToLeftEnd() + $(mq.el()).find('textarea').trigger('paste').val('√2').trigger('input'); + assertLatex('\\sqrt{ }21+'); + }) + }); + + suite('sqrt text', function() { + test('sqrt symbol in empty latex', function() { + $(mq.el()).find('textarea').trigger('paste').val('sqrt').trigger('input'); + assertLatex('sqrt'); + }) + test('sqrt symbol in non-empty latex', function() { + mq.latex('1+'); + $(mq.el()).find('textarea').trigger('paste').val('sqrt').trigger('input'); + assertLatex('1+sqrt'); + }) + test('sqrt symbol at start of non-empty latex', function () { + mq.latex('1+'); + mq.moveToLeftEnd() + $(mq.el()).find('textarea').trigger('paste').val('sqrt').trigger('input'); + assertLatex('sqrt1+'); + }) + }); + +}); + + From a70b707c0d1fe58da46550613ec26d942c8705a2 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Fri, 16 Aug 2019 17:11:29 -0400 Subject: [PATCH 314/393] Add ARIA to style indicators In particular, overline. --- src/commands/math/commands.js | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index cba39f42f..0a44ef641 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -81,24 +81,26 @@ var SVG_SYMBOLS = { }; var Style = P(MathCommand, function(_, super_) { - _.init = function(ctrlSeq, tagName, attrs) { + _.init = function(ctrlSeq, tagName, attrs, ariaLabel) { super_.init.call(this, ctrlSeq, '<'+tagName+' '+attrs+'>&0'); + _.ariaLabel = ariaLabel || ctrlSeq; + _.mathspeakTemplate = ['Start' + _.ariaLabel + ',', 'End' + _.ariaLabel]; }; }); //fonts -LatexCmds.mathrm = bind(Style, '\\mathrm', 'span', 'class="mq-roman mq-font"'); -LatexCmds.mathit = bind(Style, '\\mathit', 'i', 'class="mq-font"'); -LatexCmds.mathbf = bind(Style, '\\mathbf', 'b', 'class="mq-font"'); -LatexCmds.mathsf = bind(Style, '\\mathsf', 'span', 'class="mq-sans-serif mq-font"'); -LatexCmds.mathtt = bind(Style, '\\mathtt', 'span', 'class="mq-monospace mq-font"'); +LatexCmds.mathrm = bind(Style, '\\mathrm', 'span', 'class="mq-roman mq-font"', 'Roman Font'); +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'); //text-decoration -LatexCmds.underline = bind(Style, '\\underline', 'span', 'class="mq-non-leaf mq-underline"'); -LatexCmds.overline = LatexCmds.bar = bind(Style, '\\overline', 'span', 'class="mq-non-leaf mq-overline"'); -LatexCmds.overrightarrow = bind(Style, '\\overrightarrow', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-right"'); -LatexCmds.overleftarrow = bind(Style, '\\overleftarrow', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-left"'); -LatexCmds.overleftrightarrow = bind(Style, '\\overleftrightarrow ', 'span', 'class="mq-non-leaf mq-overarrow mq-arrow-leftright"'); -LatexCmds.overarc = bind(Style, '\\overarc', 'span', 'class="mq-non-leaf mq-overarc"'); +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'); // `\textcolor{color}{math}` will apply a color to the given math content, where // `color` is any valid CSS Color Value (see [SitePoint docs][] (recommended), From 34c871af0d34e00c39d7733b1598486d49047800 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Fri, 16 Aug 2019 17:27:35 -0400 Subject: [PATCH 315/393] Add overline rendering test --- test/unit/typing.test.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index a22bb62d2..c3f0fc7a1 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -1282,7 +1282,12 @@ suite('typing with auto-replaces', function() { mq.latex('\\%\\operatorname{of}'); assertLatex('\\%\\operatorname{of}'); }); + + test('overline renders as expected', function() { + mq.latex('0.3\\overline{5}'); + assertLatex('0.3\\overline{5}'); + assertMathspeak('0 .3 StartOverline 5 EndOverline'); + }); }); }); - From 77e115c1324a14941682af85cbb54c8c300c4f3c Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Fri, 16 Aug 2019 18:11:36 -0400 Subject: [PATCH 316/393] Slice off leading backslash when falling back to ctrlSeq for command ARIA labels --- src/commands/math/commands.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 0a44ef641..2dce91074 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -83,7 +83,7 @@ var SVG_SYMBOLS = { var Style = P(MathCommand, function(_, super_) { _.init = function(ctrlSeq, tagName, attrs, ariaLabel) { super_.init.call(this, ctrlSeq, '<'+tagName+' '+attrs+'>&0'); - _.ariaLabel = ariaLabel || ctrlSeq; + _.ariaLabel = ariaLabel || ctrlSeq.slice(1); _.mathspeakTemplate = ['Start' + _.ariaLabel + ',', 'End' + _.ariaLabel]; }; }); @@ -353,7 +353,7 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { var SummationNotation = P(MathCommand, function(_, super_) { _.init = function(ch, html, ariaLabel) { - _.ariaLabel = ariaLabel || ctrlSeq; + _.ariaLabel = ariaLabel || ctrlSeq.slice(1); var htmlTemplate = '' + '&1' From e2d0a03955af57e9ff7583ae4c95490fa58023d3 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Sat, 17 Aug 2019 11:43:02 -0400 Subject: [PATCH 317/393] Don't use the ARIA math role for staticmath on the Mac Windows screen readers along with VoiceOver on iOS will expose our hidden speech-friendly mathspeak representations of static math when the container's role is set to math. The exact opposite is true for the Apple Mac, so if the user is on this platform, don't set the math role. --- src/services/textarea.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/services/textarea.js b/src/services/textarea.js index 0b7d5eeba..95da01eea 100644 --- a/src/services/textarea.js +++ b/src/services/textarea.js @@ -72,7 +72,13 @@ Controller.open(function(_) { if (text) textarea.select(); }; var ariaLabel = ctrlr && ctrlr.ariaLabel !== 'Math Input' ? ctrlr.ariaLabel + ': ' : ''; - ctrlr.container.attr('role', 'math').attr('aria-label', ariaLabel + root.mathspeak().trim()); + ctrlr.container.attr('aria-label', ariaLabel + root.mathspeak().trim()); + // This is gross, but necessary. + var userAgent = navigator.userAgent || navigator.vendor || window.opera; + var isIOS = /iPad|iPhone|iPod/.test(userAgent) && !window.Stream; + var isMac = navigator.appVersion.indexOf("Mac") !== -1 && !isIOS; + if (!isMac) + ctrlr.container.attr('role', 'math'); }; Options.p.substituteKeyboardEvents = saneKeyboardEvents; _.editablesTextareaEvents = function() { From e9dcbb8c4eaf891af7ba1c305e2d5201bc9e454d Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 20 Aug 2019 17:01:30 -0400 Subject: [PATCH 318/393] Add explanatory comments to math role issue in source code. --- src/services/textarea.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/services/textarea.js b/src/services/textarea.js index 95da01eea..355a611da 100644 --- a/src/services/textarea.js +++ b/src/services/textarea.js @@ -73,7 +73,18 @@ Controller.open(function(_) { }; var ariaLabel = ctrlr && ctrlr.ariaLabel !== 'Math Input' ? ctrlr.ariaLabel + ': ' : ''; ctrlr.container.attr('aria-label', ariaLabel + root.mathspeak().trim()); + // This is gross, but necessary. + // On Windows, ChromeOS, Android, and iOS, supplying role="math" allows users of + // JAWS, NVDA, ChromeVox, Talkback, and VoiceOver to read the mathspeak version of an equation + // which we supply as the container's aria-label attribute. + // Omitting role="math" makes the container invisible to JAWS and iOS VoiceOver. + // At time of writing (8/20/2019), the exact opposite is true of the Mac-- + // Supplying role="math" makes the content of the container invisible to VoiceOver there, + // and omitting it makes the material available to Mac users. + // For now, the solution is to render role="math" unless the user is on Mac. + // Bug report: https://feedbackassistant.apple.com/feedback/7076111 + var userAgent = navigator.userAgent || navigator.vendor || window.opera; var isIOS = /iPad|iPhone|iPod/.test(userAgent) && !window.Stream; var isMac = navigator.appVersion.indexOf("Mac") !== -1 && !isIOS; From 374919a4e804812b83fb93d923050194e605cd63 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 20 Aug 2019 17:47:43 -0400 Subject: [PATCH 319/393] Use safer regexp replaces to weed out possible leading backslach chars from ctrl sequences --- src/commands/math.js | 2 +- src/commands/math/commands.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/commands/math.js b/src/commands/math.js index e796c31d0..f0a187a6d 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -306,7 +306,7 @@ var MathCommand = P(MathElement, function(_, super_) { */ var Symbol = P(MathCommand, function(_, super_) { _.init = function(ctrlSeq, html, text, mathspeak) { - if (!text) text = ctrlSeq && ctrlSeq.length > 1 ? ctrlSeq.slice(1) : ctrlSeq; + if (!text && !!ctrlSeq) text = ctrlSeq.replace(/^\\/, ''); this.mathspeakName = mathspeak || text; super_.init.call(this, ctrlSeq, html, [ text ]); diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 2dce91074..45672ac46 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -83,7 +83,7 @@ var SVG_SYMBOLS = { var Style = P(MathCommand, function(_, super_) { _.init = function(ctrlSeq, tagName, attrs, ariaLabel) { super_.init.call(this, ctrlSeq, '<'+tagName+' '+attrs+'>&0'); - _.ariaLabel = ariaLabel || ctrlSeq.slice(1); + _.ariaLabel = ariaLabel || ctrlSeq.replace(/^\\/, ''); _.mathspeakTemplate = ['Start' + _.ariaLabel + ',', 'End' + _.ariaLabel]; }; }); @@ -353,7 +353,7 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { var SummationNotation = P(MathCommand, function(_, super_) { _.init = function(ch, html, ariaLabel) { - _.ariaLabel = ariaLabel || ctrlSeq.slice(1); + _.ariaLabel = ariaLabel || ctrlSeq.replace(/^\\/, ''); var htmlTemplate = '' + '&1' @@ -874,13 +874,13 @@ LatexCmds.left = P(MathCommand, function(_) { return optWhitespace.then(regex(/^(?:[([|]|\\\{|\\langle\b|\\lVert\b)/)) .then(function(ctrlSeq) { - var open = (ctrlSeq.charAt(0) === '\\' ? ctrlSeq.slice(1) : ctrlSeq); + var open = ctrlSeq.replace(/^\\/, ''); if (ctrlSeq=="\\langle") { open = '⟨'; ctrlSeq = ctrlSeq + ' '; } if (ctrlSeq=="\\lVert") { open = '∥'; ctrlSeq = ctrlSeq + ' '; } return latexMathParser.then(function (block) { return string('\\right').skip(optWhitespace) .then(regex(/^(?:[\])|]|\\\}|\\rangle\b|\\rVert\b)/)).map(function(end) { - var close = (end.charAt(0) === '\\' ? end.slice(1) : end); + var close = end.replace(/^\\/, ''); if (end=="\\rangle") { close = '⟩'; end = end + ' '; } if (end=="\\rVert") { close = '∥'; end = end + ' '; } var cmd = Bracket(0, open, close, ctrlSeq, end); From c863eb52a6e334e2525748e38c3ae9a4195ec759 Mon Sep 17 00:00:00 2001 From: Eli Luberoff Date: Mon, 9 Sep 2019 11:40:49 -0700 Subject: [PATCH 320/393] always wrap sup/sub with brackets --- src/commands/math/commands.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 45672ac46..469fd7648 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -258,7 +258,7 @@ var SupSub = P(MathCommand, function(_, super_) { _.latex = function() { function latex(prefix, block) { var l = block && block.latex(); - return block ? prefix + (l.length === 1 ? l : '{' + (l || ' ') + '}') : ''; + return block ? prefix + '{' + (l || ' ') + '}' : ''; } return latex('_', this.sub) + latex('^', this.sup); }; @@ -372,7 +372,7 @@ var SummationNotation = P(MathCommand, function(_, super_) { }; _.latex = function() { function simplify(latex) { - return latex.length === 1 ? latex : '{' + (latex || ' ') + '}'; + return '{' + (latex || ' ') + '}'; } return this.ctrlSeq + '_' + simplify(this.ends[L].latex()) + '^' + simplify(this.ends[R].latex()); From c62550d7f327ec16ffe31857a8a8ed4e458d4f49 Mon Sep 17 00:00:00 2001 From: Eli Luberoff Date: Fri, 13 Sep 2019 17:51:41 -0700 Subject: [PATCH 321/393] update tests --- test/unit/SupSub.test.js | 54 +++++++++++++++++---------------- test/unit/autosubscript.test.js | 14 ++++----- test/unit/backspace.test.js | 24 +++++++-------- test/unit/latex.test.js | 40 ++++++++++++------------ test/unit/publicapi.test.js | 48 ++++++++++++++--------------- test/unit/typing.test.js | 16 +++++----- 6 files changed, 99 insertions(+), 97 deletions(-) diff --git a/test/unit/SupSub.test.js b/test/unit/SupSub.test.js index 45d5a23a5..4e82866e1 100644 --- a/test/unit/SupSub.test.js +++ b/test/unit/SupSub.test.js @@ -7,15 +7,15 @@ suite('SupSub', function() { function prayWellFormedPoint(pt) { prayWellFormed(pt.parent, pt[L], pt[R]); } var expecteds = [ - 'x_{ab} x_{ba}, x_a^b x_a^b; x_{ab} x_{ba}, x_a^b x_a^b; x_a x_a, x_a^{} x_a^{}', - 'x_b^a x_b^a, x^{ab} x^{ba}; x_b^a x_b^a, x^{ab} x^{ba}; x_{}^a x_{}^a, x^a x^a' + 'x_{ab} x_{ba}, x_{a}^{b} x_{a}^{b}; x_{ab} x_{ba}, x_{a}^{b} x_{a}^{b}; x_{a} x_{a}, x_{a}^{} x_{a}^{}', + 'x_{b}^{a} x_{b}^{a}, x^{ab} x^{ba}; x_{b}^{a} x_{b}^{a}, x^{ab} x^{ba}; x_{}^{a} x_{}^{a}, x^{a} x^{a}' ]; var expectedsAfterC = [ - 'x_{abc} x_{bca}, x_a^{bc} x_a^{bc}; x_{ab}c x_{bca}, x_a^bc x_a^bc; x_ac x_{ca}, x_a^{}c x_a^{}c', - 'x_{bc}^a x_{bc}^a, x^{abc} x^{bca}; x_b^ac x_b^ac, x^{ab}c x^{bca}; x_{}^ac x_{}^ac, x^ac x^{ca}' + 'x_{abc} x_{bca}, x_{a}^{bc} x_{a}^{bc}; x_{ab}c x_{bca}, x_{a}^{b}c x_{a}^{b}c; x_{a}c x_{ca}, x_{a}^{}c x_{a}^{}c', + 'x_{bc}^{a} x_{bc}^{a}, x^{abc} x^{bca}; x_{b}^{a}c x_{b}^{a}c, x^{ab}c x^{bca}; x_{}^{a}c x_{}^{a}c, x^{a}c x^{ca}' ]; 'sub super'.split(' ').forEach(function(initSupsub, i) { - var initialLatex = 'x_a x^a'.split(' ')[i]; + var initialLatex = 'x_{a} x^{a}'.split(' ')[i]; 'typed, wrote, wrote empty'.split(', ').forEach(function(did, j) { var doTo = [ @@ -55,10 +55,12 @@ suite('SupSub', function() { }); }); - var expecteds = 'x_a^3 x_a^3, x_a^3 x_a^3; x^{a3} x^{3a}, x^{a3} x^{3a}'; - var expectedsAfterC = 'x_a^3c x_a^3c, x_a^3c x_a^3c; x^{a3}c x^{3ca}, x^{a3}c x^{3ca}'; + + + var expecteds = 'x_{a}^{3} x_{a}^{3}, x_{a}^{3} x_{a}^{3}; x^{a3} x^{3a}, x^{a3} x^{3a}'; + var expectedsAfterC = 'x_{a}^{3}c x_{a}^{3}c, x_{a}^{3}c x_{a}^{3}c; x^{a3}c x^{3ca}, x^{a3}c x^{3ca}'; 'sub super'.split(' ').forEach(function(initSupsub, i) { - var initialLatex = 'x_a x^a'.split(' ')[i]; + var initialLatex = 'x_{a} x^{a}'.split(' ')[i]; 'typed wrote'.split(' ').forEach(function(did, j) { var doTo = [ @@ -98,67 +100,67 @@ suite('SupSub', function() { assert.equal(mq.latex(), 'x_{ab}'); mq.latex('x_a_{}'); - assert.equal(mq.latex(), 'x_a'); + assert.equal(mq.latex(), 'x_{a}'); mq.latex('x_{}_a'); - assert.equal(mq.latex(), 'x_a'); + assert.equal(mq.latex(), 'x_{a}'); mq.latex('x^a^b'); assert.equal(mq.latex(), 'x^{ab}'); mq.latex('x^a^{}'); - assert.equal(mq.latex(), 'x^a'); + assert.equal(mq.latex(), 'x^{a}'); mq.latex('x^{}^a'); - assert.equal(mq.latex(), 'x^a'); + assert.equal(mq.latex(), 'x^{a}'); }); test('render LaTeX with 3 alternating SupSub\'s in a row', function() { mq.latex('x_a^b_c'); - assert.equal(mq.latex(), 'x_{ac}^b'); + assert.equal(mq.latex(), 'x_{ac}^{b}'); mq.latex('x^a_b^c'); - assert.equal(mq.latex(), 'x_b^{ac}'); + assert.equal(mq.latex(), 'x_{b}^{ac}'); }); suite('deleting', function() { test('backspacing out of and then re-typing subscript', function() { mq.latex('x_a^b'); - assert.equal(mq.latex(), 'x_a^b'); + assert.equal(mq.latex(), 'x_{a}^{b}'); mq.keystroke('Down Backspace'); - assert.equal(mq.latex(), 'x_{ }^b'); + assert.equal(mq.latex(), 'x_{ }^{b}'); mq.keystroke('Backspace'); - assert.equal(mq.latex(), 'x^b'); + assert.equal(mq.latex(), 'x^{b}'); mq.typedText('_a'); - assert.equal(mq.latex(), 'x_a^b'); + assert.equal(mq.latex(), 'x_{a}^{b}'); mq.keystroke('Left Backspace'); - assert.equal(mq.latex(), 'xa^b'); + assert.equal(mq.latex(), 'xa^{b}'); mq.typedText('c'); - assert.equal(mq.latex(), 'xca^b'); + assert.equal(mq.latex(), 'xca^{b}'); }); test('backspacing out of and then re-typing superscript', function() { mq.latex('x_a^b'); - assert.equal(mq.latex(), 'x_a^b'); + assert.equal(mq.latex(), 'x_{a}^{b}'); mq.keystroke('Up Backspace'); - assert.equal(mq.latex(), 'x_a^{ }'); + assert.equal(mq.latex(), 'x_{a}^{ }'); mq.keystroke('Backspace'); - assert.equal(mq.latex(), 'x_a'); + assert.equal(mq.latex(), 'x_{a}'); mq.typedText('^b'); - assert.equal(mq.latex(), 'x_a^b'); + assert.equal(mq.latex(), 'x_{a}^{b}'); mq.keystroke('Left Backspace'); - assert.equal(mq.latex(), 'x_ab'); + assert.equal(mq.latex(), 'x_{a}b'); mq.typedText('c'); - assert.equal(mq.latex(), 'x_acb'); + assert.equal(mq.latex(), 'x_{a}cb'); }); }); }); diff --git a/test/unit/autosubscript.test.js b/test/unit/autosubscript.test.js index 3680d314b..d4945aac7 100644 --- a/test/unit/autosubscript.test.js +++ b/test/unit/autosubscript.test.js @@ -10,7 +10,7 @@ suite('autoSubscript', function() { test('auto subscripting variables', function() { mq.latex('x'); mq.typedText('2'); - assert.equal(mq.latex(), 'x_2'); + assert.equal(mq.latex(), 'x_{2}'); mq.typedText('3'); assert.equal(mq.latex(), 'x_{23}'); }); @@ -26,17 +26,17 @@ suite('autoSubscript', function() { test('autosubscript exponentiated variables', function() { mq.latex('x^2'); mq.typedText('2'); - assert.equal(mq.latex(), 'x_2^2'); + assert.equal(mq.latex(), 'x_{2}^{2}'); mq.typedText('3'); - assert.equal(mq.latex(), 'x_{23}^2'); + assert.equal(mq.latex(), 'x_{23}^{2}'); }); test('do not autosubscript exponentiated functions', function() { mq.latex('sin^{2}'); mq.typedText('2'); - assert.equal(mq.latex(), '\\sin^22'); + assert.equal(mq.latex(), '\\sin^{2}2'); mq.typedText('3'); - assert.equal(mq.latex(), '\\sin^223'); + assert.equal(mq.latex(), '\\sin^{2}23'); }); test('do not autosubscript subscripted functions', function() { @@ -51,7 +51,7 @@ suite('autoSubscript', function() { //first backspace moves to cursor in subscript and peels it off mq.keystroke('Backspace'); - assert.equal(mq.latex(),'x_2'); + assert.equal(mq.latex(),'x_{2}'); //second backspace clears out remaining subscript mq.keystroke('Backspace'); @@ -72,7 +72,7 @@ suite('autoSubscript', function() { assert.equal(mq.latex(),'x_{2+}'); assert.equal(cursor.parent, rootBlock, 'backspace keeps us in the root block'); mq.keystroke('Backspace'); - assert.equal(mq.latex(),'x_2'); + assert.equal(mq.latex(),'x_{2}'); assert.equal(cursor.parent, rootBlock, 'backspace keeps us in the root block'); //second backspace clears out remaining subscript and unpeels diff --git a/test/unit/backspace.test.js b/test/unit/backspace.test.js index b76ec5269..bd8311a37 100644 --- a/test/unit/backspace.test.js +++ b/test/unit/backspace.test.js @@ -27,7 +27,7 @@ suite('backspace', function() { mq.keystroke('Backspace'); assert.equal(cursor.parent, expBlock, 'cursor still in exponent'); - assertLatex('x^n'); + assertLatex('x^{n}'); mq.keystroke('Backspace'); assert.equal(cursor.parent, expBlock, 'still in exponent, but it is empty'); @@ -82,17 +82,17 @@ suite('backspace', function() { //first backspace goes into the subscript mq.keystroke('Backspace'); - assert.equal(mq.latex(),'x_{2_2}'); + assert.equal(mq.latex(),'x_{2_{2}}'); //second one goes into the subscripts' subscript mq.keystroke('Backspace'); - assert.equal(mq.latex(),'x_{2_2}'); + assert.equal(mq.latex(),'x_{2_{2}}'); mq.keystroke('Backspace'); assert.equal(mq.latex(),'x_{2_{ }}'); mq.keystroke('Backspace'); - assert.equal(mq.latex(),'x_2'); + assert.equal(mq.latex(),'x_{2}'); mq.keystroke('Backspace'); assert.equal(mq.latex(),'x_{ }'); @@ -112,7 +112,7 @@ suite('backspace', function() { mq.keystroke('Backspace'); assert.equal(mq.latex(),'x_{2+}'); mq.keystroke('Backspace'); - assert.equal(mq.latex(),'x_2'); + assert.equal(mq.latex(),'x_{2}'); mq.keystroke('Backspace'); assert.equal(mq.latex(),'x_{ }'); mq.keystroke('Backspace'); @@ -124,23 +124,23 @@ suite('backspace', function() { //first backspace takes us into the exponent mq.keystroke('Backspace'); - assert.equal(mq.latex(),'x_2^{32}'); + assert.equal(mq.latex(),'x_{2}^{32}'); //second backspace is within the exponent mq.keystroke('Backspace'); - assert.equal(mq.latex(),'x_2^3'); + assert.equal(mq.latex(),'x_{2}^{3}'); //clear out exponent mq.keystroke('Backspace'); - assert.equal(mq.latex(),'x_2^{ }'); + assert.equal(mq.latex(),'x_{2}^{ }'); //unpeel exponent mq.keystroke('Backspace'); - assert.equal(mq.latex(),'x_2'); + assert.equal(mq.latex(),'x_{2}'); //into subscript mq.keystroke('Backspace'); - assert.equal(mq.latex(),'x_2'); + assert.equal(mq.latex(),'x_{2}'); //clear out subscript mq.keystroke('Backspace'); @@ -179,11 +179,11 @@ suite('backspace', function() { //first backspace takes out the argument mq.keystroke('Backspace'); - assert.equal(mq.latex(),'\\sum_{n=1}^3'); + assert.equal(mq.latex(),'\\sum_{n=1}^{3}'); //up into the superscript mq.keystroke('Backspace'); - assert.equal(mq.latex(),'\\sum_{n=1}^3'); + assert.equal(mq.latex(),'\\sum_{n=1}^{3}'); //up into the superscript mq.keystroke('Backspace'); diff --git a/test/unit/latex.test.js b/test/unit/latex.test.js index 56a63f859..494389bb6 100644 --- a/test/unit/latex.test.js +++ b/test/unit/latex.test.js @@ -26,23 +26,23 @@ suite('latex', function() { }); test('simple exponent', function() { - assertParsesLatex('x^n'); + assertParsesLatex('x^{n}'); }); test('block exponent', function() { - assertParsesLatex('x^{n}', 'x^n'); + assertParsesLatex('x^{n}', 'x^{n}'); assertParsesLatex('x^{nm}'); assertParsesLatex('x^{}', 'x^{ }'); }); test('nested exponents', function() { - assertParsesLatex('x^{n^m}'); + assertParsesLatex('x^{n^{m}}'); }); test('exponents with spaces', function() { - assertParsesLatex('x^ 2', 'x^2'); + assertParsesLatex('x^ 2', 'x^{2}'); - assertParsesLatex('x ^2', 'x^2'); + assertParsesLatex('x ^2', 'x^{2}'); }); test('inner groups', function() { @@ -154,13 +154,13 @@ suite('latex', function() { test('basic rendering', function() { assertParsesLatex('x = \\frac{ -b \\pm \\sqrt{ b^2 - 4ac } }{ 2a }', - 'x=\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}'); + 'x=\\frac{-b\\pm\\sqrt{b^{2}-4ac}}{2a}'); }); test('re-rendering', function() { - assertParsesLatex('a x^2 + b x + c = 0', 'ax^2+bx+c=0'); + assertParsesLatex('a x^2 + b x + c = 0', 'ax^{2}+bx+c=0'); assertParsesLatex('x = \\frac{ -b \\pm \\sqrt{ b^2 - 4ac } }{ 2a }', - 'x=\\frac{-b\\pm\\sqrt{b^2-4ac}}{2a}'); + 'x=\\frac{-b\\pm\\sqrt{b^{2}-4ac}}{2a}'); }); test('empty LaTeX', function () { @@ -227,23 +227,23 @@ suite('latex', function() { suite('\\sum', function() { test('basic', function() { mq.write('\\sum_{n=0}^5'); - assert.equal(mq.latex(), '\\sum_{n=0}^5'); + assert.equal(mq.latex(), '\\sum_{n=0}^{5}'); mq.write('x^n'); - assert.equal(mq.latex(), '\\sum_{n=0}^5x^n'); + assert.equal(mq.latex(), '\\sum_{n=0}^{5}x^{n}'); }); test('only lower bound', function() { mq.write('\\sum_{n=0}'); assert.equal(mq.latex(), '\\sum_{n=0}^{ }'); mq.write('x^n'); - assert.equal(mq.latex(), '\\sum_{n=0}^{ }x^n'); + assert.equal(mq.latex(), '\\sum_{n=0}^{ }x^{n}'); }); test('only upper bound', function() { mq.write('\\sum^5'); - assert.equal(mq.latex(), '\\sum_{ }^5'); + assert.equal(mq.latex(), '\\sum_{ }^{5}'); mq.write('x^n'); - assert.equal(mq.latex(), '\\sum_{ }^5x^n'); + assert.equal(mq.latex(), '\\sum_{ }^{5}x^{n}'); }); }); }); @@ -261,25 +261,25 @@ suite('latex', function() { }); test('initial latex', function() { - assert.equal(inner1.latex(), 'x_0+x_1+x_2'); + assert.equal(inner1.latex(), 'x_{0}+x_{1}+x_{2}'); assert.equal(inner2.latex(), '3'); - assert.equal(outer.latex(), '\\frac{x_0+x_1+x_2}{3}'); + assert.equal(outer.latex(), '\\frac{x_{0}+x_{1}+x_{2}}{3}'); }); test('setting latex', function() { inner1.latex('\\sum_{i=0}^N x_i'); inner2.latex('N'); - assert.equal(inner1.latex(), '\\sum_{i=0}^Nx_i'); + assert.equal(inner1.latex(), '\\sum_{i=0}^{N}x_{i}'); assert.equal(inner2.latex(), 'N'); - assert.equal(outer.latex(), '\\frac{\\sum_{i=0}^Nx_i}{N}'); + assert.equal(outer.latex(), '\\frac{\\sum_{i=0}^{N}x_{i}}{N}'); }); test('writing latex', function() { inner1.write('+ x_3'); inner2.write('+ 1'); - assert.equal(inner1.latex(), 'x_0+x_1+x_2+x_3'); + assert.equal(inner1.latex(), 'x_{0}+x_{1}+x_{2}+x_{3}'); assert.equal(inner2.latex(), '3+1'); - assert.equal(outer.latex(), '\\frac{x_0+x_1+x_2+x_3}{3+1}'); + assert.equal(outer.latex(), '\\frac{x_{0}+x_{1}+x_{2}+x_{3}}{3+1}'); }); test('optional inner field name', function() { @@ -297,7 +297,7 @@ suite('latex', function() { mantissa.latex('1.2345'); base.latex('10'); exp.latex('8'); - assert.equal(outer.latex(), '1.2345\\cdot10^8'); + assert.equal(outer.latex(), '1.2345\\cdot10^{8}'); }); test('separate API object', function() { diff --git a/test/unit/publicapi.test.js b/test/unit/publicapi.test.js index ff78c9b08..8efca6e23 100644 --- a/test/unit/publicapi.test.js +++ b/test/unit/publicapi.test.js @@ -372,11 +372,11 @@ suite('Public API', function() { mq.cmd('^'); assert.equal(mq.latex(), 'xy^{ }'); mq.cmd('2'); - assert.equal(mq.latex(), 'xy^2'); + assert.equal(mq.latex(), 'xy^{2}'); mq.keystroke('Right Shift-Left Shift-Left Shift-Left').cmd('\\sqrt'); - assert.equal(mq.latex(), '\\sqrt{xy^2}'); + assert.equal(mq.latex(), '\\sqrt{xy^{2}}'); mq.typedText('*2**'); - assert.equal(mq.latex(), '\\sqrt{xy^2\\cdot2\\cdot\\cdot}'); + assert.equal(mq.latex(), '\\sqrt{xy^{2}\\cdot2\\cdot\\cdot}'); }); test('backslash commands are passed their name', function() { @@ -508,7 +508,7 @@ suite('Public API', function() { + 'thegraphicalelementsofadocumentorvisualpresentation.'); }); test('actual LaTeX', function() { - assertPaste('a_nx^n+a_{n+1}x^{n+1}'); + assertPaste('a_{n}x^{n}+a_{n+1}x^{n+1}'); assertPaste('\\frac{1}{2\\sqrt{x}}'); }); test('\\text{...}', function() { @@ -519,7 +519,7 @@ suite('Public API', function() { test('selection', function(done) { mq.latex('x^2').select(); setTimeout(function() { - assert.equal(textarea.val(), 'x^2'); + assert.equal(textarea.val(), 'x^{2}'); done(); }); }); @@ -555,13 +555,13 @@ suite('Public API', function() { }); // TODO: braces (currently broken) test('actual math LaTeX wrapped in dollar signs', function() { - assertPaste('$a_nx^n+a_{n+1}x^{n+1}$', 'a_nx^n+a_{n+1}x^{n+1}'); + assertPaste('$a_nx^n+a_{n+1}x^{n+1}$', 'a_{n}x^{n}+a_{n+1}x^{n+1}'); assertPaste('$\\frac{1}{2\\sqrt{x}}$', '\\frac{1}{2\\sqrt{x}}'); }); test('selection', function(done) { mq.latex('x^2').select(); setTimeout(function() { - assert.equal(textarea.val(), '$x^2$'); + assert.equal(textarea.val(), '$x^{2}$'); done(); }); }); @@ -626,25 +626,25 @@ suite('Public API', function() { test('supsub', function() { mq.latex('x_a+y^b+z_a^b+w'); - assert.equal(mq.latex(), 'x_a+y^b+z_a^b+w'); + assert.equal(mq.latex(), 'x_{a}+y^{b}+z_{a}^{b}+w'); mq.moveToLeftEnd().typedText('1'); - assert.equal(mq.latex(), '1x_a+y^b+z_a^b+w'); + assert.equal(mq.latex(), '1x_{a}+y^{b}+z_{a}^{b}+w'); mq.keystroke('Right Right').typedText('2'); - assert.equal(mq.latex(), '1x_{2a}+y^b+z_a^b+w'); + assert.equal(mq.latex(), '1x_{2a}+y^{b}+z_{a}^{b}+w'); mq.keystroke('Right Right').typedText('3'); - assert.equal(mq.latex(), '1x_{2a}3+y^b+z_a^b+w'); + assert.equal(mq.latex(), '1x_{2a}3+y^{b}+z_{a}^{b}+w'); mq.keystroke('Right Right Right').typedText('4'); - assert.equal(mq.latex(), '1x_{2a}3+y^{4b}+z_a^b+w'); + assert.equal(mq.latex(), '1x_{2a}3+y^{4b}+z_{a}^{b}+w'); mq.keystroke('Right Right').typedText('5'); - assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_a^b+w'); + assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_{a}^{b}+w'); mq.keystroke('Right Right Right').typedText('6'); - assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_{6a}^b+w'); + assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_{6a}^{b}+w'); mq.keystroke('Right Right').typedText('7'); assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_{6a}^{7b}+w'); @@ -706,28 +706,28 @@ suite('Public API', function() { test('supsub', function() { mq.latex('x_a+y^b+z_a^b+w'); - assert.equal(mq.latex(), 'x_a+y^b+z_a^b+w'); + assert.equal(mq.latex(), 'x_{a}+y^{b}+z_{a}^{b}+w'); mq.moveToLeftEnd().typedText('1'); - assert.equal(mq.latex(), '1x_a+y^b+z_a^b+w'); + assert.equal(mq.latex(), '1x_{a}+y^{b}+z_{a}^{b}+w'); mq.keystroke('Right Right').typedText('2'); - assert.equal(mq.latex(), '1x_{2a}+y^b+z_a^b+w'); + assert.equal(mq.latex(), '1x_{2a}+y^{b}+z_{a}^{b}+w'); mq.keystroke('Right Right').typedText('3'); - assert.equal(mq.latex(), '1x_{2a}3+y^b+z_a^b+w'); + assert.equal(mq.latex(), '1x_{2a}3+y^{b}+z_{a}^{b}+w'); mq.keystroke('Right Right Right').typedText('4'); - assert.equal(mq.latex(), '1x_{2a}3+y^{4b}+z_a^b+w'); + assert.equal(mq.latex(), '1x_{2a}3+y^{4b}+z_{a}^{b}+w'); mq.keystroke('Right Right').typedText('5'); - assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_a^b+w'); + assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_{a}^{b}+w'); mq.keystroke('Right Right Right').typedText('6'); - assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_a^{6b}+w'); + assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_{a}^{6b}+w'); mq.keystroke('Right Right').typedText('7'); - assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_a^{6b}7+w'); + assert.equal(mq.latex(), '1x_{2a}3+y^{4b}5+z_{a}^{6b}7+w'); }); test('nthroot', function() { @@ -758,7 +758,7 @@ suite('Public API', function() { assert.equal(mq.latex(), '\\sum_{ }^{ }'); mq.cmd('n'); - assert.equal(mq.latex(), '\\sum_n^{ }', 'cursor in lower limit'); + assert.equal(mq.latex(), '\\sum_{n}^{ }', 'cursor in lower limit'); }); test('sum starts with `n=`', function() { var mq = MQ.MathField($('').appendTo('#mock')[0], { @@ -782,7 +782,7 @@ suite('Public API', function() { assert.equal(mq.latex(), '\\int_{ }^{ }'); mq.cmd('0'); - assert.equal(mq.latex(), '\\int_0^{ }', 'cursor in the from block'); + assert.equal(mq.latex(), '\\int_{0}^{ }', 'cursor in the from block'); }); }); diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index c3f0fc7a1..f8f951d82 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -61,7 +61,7 @@ suite('typing with auto-replaces', function() { test('auto-operator names', function() { mq.typedText('\\sin^2'); - assertLatex('\\sin^2'); + assertLatex('\\sin^{2}'); }); test('nonexistent LaTeX command', function() { @@ -1203,22 +1203,22 @@ suite('typing with auto-replaces', function() { }); test('supSubsRequireOperand', function() { assert.equal(mq.typedText('^').latex(), '^{ }'); - assert.equal(mq.typedText('2').latex(), '^2'); + assert.equal(mq.typedText('2').latex(), '^{2}'); assert.equal(mq.typedText('n').latex(), '^{2n}'); mq.latex(''); assert.equal(mq.typedText('x').latex(), 'x'); assert.equal(mq.typedText('^').latex(), 'x^{ }'); - assert.equal(mq.typedText('2').latex(), 'x^2'); + assert.equal(mq.typedText('2').latex(), 'x^{2}'); assert.equal(mq.typedText('n').latex(), 'x^{2n}'); mq.latex(''); assert.equal(mq.typedText('x').latex(), 'x'); assert.equal(mq.typedText('^').latex(), 'x^{ }'); assert.equal(mq.typedText('^').latex(), 'x^{^{ }}'); - assert.equal(mq.typedText('2').latex(), 'x^{^2}'); + assert.equal(mq.typedText('2').latex(), 'x^{^{2}}'); assert.equal(mq.typedText('n').latex(), 'x^{^{2n}}'); mq.latex(''); assert.equal(mq.typedText('2').latex(), '2'); - assert.equal(mq.keystroke('Shift-Left').typedText('^').latex(), '^2'); + assert.equal(mq.keystroke('Shift-Left').typedText('^').latex(), '^{2}'); mq.latex(''); MQ.config({ supSubsRequireOperand: true }); @@ -1229,17 +1229,17 @@ suite('typing with auto-replaces', function() { mq.latex(''); assert.equal(mq.typedText('x').latex(), 'x'); assert.equal(mq.typedText('^').latex(), 'x^{ }'); - assert.equal(mq.typedText('2').latex(), 'x^2'); + assert.equal(mq.typedText('2').latex(), 'x^{2}'); assert.equal(mq.typedText('n').latex(), 'x^{2n}'); mq.latex(''); assert.equal(mq.typedText('x').latex(), 'x'); assert.equal(mq.typedText('^').latex(), 'x^{ }'); assert.equal(mq.typedText('^').latex(), 'x^{ }'); - assert.equal(mq.typedText('2').latex(), 'x^2'); + assert.equal(mq.typedText('2').latex(), 'x^{2}'); assert.equal(mq.typedText('n').latex(), 'x^{2n}'); mq.latex(''); assert.equal(mq.typedText('2').latex(), '2'); - assert.equal(mq.keystroke('Shift-Left').typedText('^').latex(), '^2'); + assert.equal(mq.keystroke('Shift-Left').typedText('^').latex(), '^{2}'); }); }); From e976422e61daf3a7739d37d6a3382eedf6ec2534 Mon Sep 17 00:00:00 2001 From: Jason Merrill Date: Thu, 7 Nov 2019 16:15:05 -0800 Subject: [PATCH 322/393] Fix a missing "var" declaration. --- src/commands/math/basicSymbols.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index 14de8316f..57078a0a8 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -111,7 +111,7 @@ var DigitGroupingChar = P(Symbol, function(_, super_) { while (node) { count += 1; - cls = undefined; + var cls = undefined; // only do grouping if we have at least 4 numbers if (totalDigits >= 4) { From 6ec1a6cba0d5d0957c9bb881df70a62e790d2509 Mon Sep 17 00:00:00 2001 From: Jason Merrill Date: Fri, 22 Nov 2019 18:36:13 -0800 Subject: [PATCH 323/393] Update Symbola-basic fonts to include the space character, because Chrome's handling of the zero width space character, which mathquill uses for its cursor and a few other purposes, is buggy without it. --- src/font/Symbola-basic.css | 2 +- src/font/Symbola-basic.ttf | Bin 17484 -> 17196 bytes src/font/Symbola-basic.woff | Bin 10356 -> 10352 bytes 3 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/font/Symbola-basic.css b/src/font/Symbola-basic.css index 0c12dd0d0..3a8186b98 100644 --- a/src/font/Symbola-basic.css +++ b/src/font/Symbola-basic.css @@ -1,4 +1,4 @@ @font-face { font-family: Symbola; - src: url(data:application/font-woff;base64,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) format('woff'); + src: url(data:application/font-woff;base64,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) format('woff'); } \ No newline at end of file diff --git a/src/font/Symbola-basic.ttf b/src/font/Symbola-basic.ttf index 3b71bd34f3c966d5dfffeecc7b0dfd4936bbcd2f..d1eda7b9e1024967d6eb69fe3597f85eda4264b7 100644 GIT binary patch delta 2164 zcmZ8h3v64}8UD__w&OT<{D_^}rVrOi>!elFVwx9iLeu6^SSt#lAgE|@5;qChiR+|{ z3`Ewkgic#Fjl&A0Ohh~clnM!M1zU@vmWM{W0$Q~VnA(g-i$Fs|0-CLA((oN$SBzcj zyWjsh|Nr}(|J;*%#lcZA3<1E)mj#}-=9X5R#b-d%Nm?t~wp`zi5S-M%rS55K-@5+E z$yWlvdJ7;bu5Vu%|JCSoTwK4A{+m0~iOlr2$wSm9=s(?g_h7_pbFBdy%jv&uXQn&- z;h~k!0&DIAv}AW;U^oLE1uSrs0nYB!(9WN~*ZfZ|o)1)??MWuOF4kSR2-Lnte@zb! z?hx_f;2Zd4XxKYSo6z!9XormQa z=XTlP8kKLkCgn)+q|xGb2;&CN&otw2zHv?N3jD*^T>6kOKAu-63M+uwGLH=vG8#H2 z&xQUNo}0q~(QX~n>pd|zfVmn^fnuDHuZ54up(|cCR?IIIM$3YK3u9}{XEA!0Kdl+n zi{e7|M#iP9YP+;9z9g;DX*nC+Vx+51=|)q$R5NZ}-KWdrwbRD2y0|dT*Kf2Ko6AN- zv8t#ou#Rc{Y{VfyDZkZdSW+wG+QyFS=o>(f^({-3-88csE4v}%f?9RyuhuckKF?Cl z0L(6DUj2dnitp`mqOneu8UU<+vfji}8|Ax8TVx=%Zdi@jCi_wR1-m#*8b?k;DYqLu zNu7fSmNb8R@djo^O1vu0YkqBmHmaS|oAu+CF{{IR*mjHU&4Mv|nSG`G*YN4?_~$EQWBoHefc;$IfO?=E-ma!+^`dB#02dS3S~^FBB1tMa{2QeE?f&J5F=8{Z!B&d{ zmIn|cmS|;o-f{<4Tlx^^M(p7F9r8=24m?R+70+P0 z(s?4+rmA=O`4NOZA`gm+EqIEyb(ig}*<_MhB=&`wz}6}b)8~3f&qt`^P;xn;SoSv4O^fe+P_v)3EpU$0`J2rQ8?y0$|*{+%N%$*+vr>>r=np!cnbZY)o@XYL) zPd<3?gANG!Tg&ocen-shR198PM2N+Ns>iE>M#4&L%Yz>Q+@8k*^1a0U1TQ%LlgAS7 z@$1AJ)R!_9KNJe6Bp)?_Wh_-P%$F*<9EkE%6tI@;Hh~7Rm+;ZfMxtR8+$=;{B+BGy zBCGWTHjp=(KnuBxNYc(ziLoICTuc6(_=2*y&Tv`_^iDf=sx7q?(x%SRlm&{ZnX*Rt zCwWl$UM)v-WND;iMeBCOzDaA*9iPe&$VZ7?bG6?wp*(kEwOA zkA6lE;)juu|BJnuv02{h-duD9aa5t2@0-v=+W-&oa-InDCUTZ;ByVGCwJ^Z~Npvx+ zgAGNf^-zy+o6O=7mfyw3dKsIgCq-Wy`TN!Vv{$K%q#h2vUw*6y3O7+v9}-tUg=d6c1n{^h z#j|)AzZ3IBP=rLdV0T~ds`!R@p0ARBZ`!wnws<^Vt!(6)f`Q(2Zz_>>Ws=!UvahSR zb2l|P*%Xv})(1DMhPB)+im}&9j*Q@_wDEl{ctQCC@063rXFjIB40X^uhq5X7yR*r= z;7=t6`}nUvTfcdDC9|n`7ixI;PL%K<6#K9$#~!todVo8oxD+dL>_=IS-H7Jc%f8gu zT$R&&xH88czU}aEwCZzu0ITzUb^D$0HnL?4Pv0zQrnovETASmNT*R8ZP2`HWnbOM^ G{oethxVKsW delta 2275 zcmZ`*du&uy8vmU;_s*;He$eT(v_plqx|No(?^bBbbc(e|rIkc9#c8>Hu$_m~A~az$ zCW_W5NVr-R@hv_G8spVn1G;E}iui~cP?kXB?y^SL-K zbI56Q8$XMQ+) zo`RK=zGjF7&WLFr@oR~nKNKI^xGqy33kL|8&fPUbdYX> z_)sF2iOVe$UwjTMK1xp3RC09e!4>aO#my(l=!sNXPW67gZ=5O~BZp>fw}j1w$4a)& zU*l{#g(5SFfrGvN-M`W4sQJ7xO1xRCgDYr*jnQe6S!g%2Uz^Wsk$?)WoO!O-M4cFg zM2Q4?8I3vImu=TLCazLC>&q4uXM=|GSz2PLh^Nibv@DauywdMyG8m(5FGaL(H}JUP zm-PLVi?w&=*J8JL7rESp1>$X}5)%lEz#3Y>8s%hN>4M!xvY;_4?7%HFMkoP{d$Cy` zONA@&vhq@0(|m43FMoIrJ__{p943yRr~>6ceIuK+{da4dextSVMs!pDG|KVQyhjh$AJ{V5ebSuqX_aU3KMx*_|&39nMQn zNhwG3E_qOPbyK6skcoi56F7irmSB6?86hAn6CM!$W9T$|DNag0>434%__=9^xzgNZ z-f2E(FalxZ8=l6a@!tznZ3#0Wq;Cs+~IQ+IF>nv94|OdI(~9Y zJFQNSv&z}y>~XDj_j+FQeB)i_{lM4hJLK;P`oEg9HW#^D1J=Ovd4+ke2RniXgXi+; zbf^XFUZy@H^e_hH7sDgUed61MBGMY=8EJx0nQ=eO-||^~-ju`CSIl<{YDLaYLHWpj zoC_TaQ>$GK%+df{yuxKfgR;^6o>J@CS4f97i`}eS+G418mBK=NjmyL-4greIyN_aj z;8oQPz5r9V_)P0c9F{6(4rj7-|pX!)*vVZAVgp=8IEWyTVb!>!* z_3%q0@;x1)F@qW9+NEQG@H?oOQ5c9hqANrc;=ekU5MXn4Y=nbFly4WuGq@=++@D-4 zhoZ^E*l1`VnGUri`iJGjSg5-{J)9aFZ49-`$@HKciX_*klf;HAE9-%7j3bUdB&ntt zvE%W+HRgnQ6OD;TN1{7G`BR%*O1@!JN#++|0wg%*Xs} z4$EZ$mdApo^@-ui@Y1kOui-Qt4p-~6Mx(V{?;u5OTiuMHtzP+e^Q5w^#jm{65?D?b zR%_(|Z}-%H^#;e=niZ|+f}wvrJp$igTK*$KBeAgr$~8NFZ@3lr(Xmq8x;%3&NpfI^ zjrx;;kO$2gbz`wcorr4GhbE2s(Wp@`>NM&=twvo`37s3XXq+2MH0q?;3>V$Fc8&K@ zjXaAR^%`~YKM*38>cW{2%XHH=je0bT>vS2TBz8*Uq@ZqiwIx}pG%EJ8g%@fIFPmNL aZ_BLUh=mT^U0mH57}N`Z;_5)G!SICPnR|eL zFpxU~$QJ-(Nd_MG;7})^*a{&3C<6nd#-e@OME!&HjTjgd*8s)*fjF$EZ3Sa;Zel&q zFvUMWMdCnw=Ueo?Ey-mi3JeUY96&`;AS}qnYn@h*p3A_XECN)sk%56J_2w4+<>`sV zK)vcIKs7vI%*?=^o>Q3yRMWt~AQ#ENVEUxxyvzNJ)I^{f1&{;HK=`-C&)*q9L7=@o z3=H)Q3P3>>eqqLw8M!4DK!q28{M|r9^zuaIMRM|!fePh;4rG`G!g8yGPsL`&^J;T*APh<^$x50rA|#H&1656r}?7s+<6- za{%Eu=F__?H*>Hh%1xG3lb&3mx{w_h_S`vB&Q2~+(Vx6uW#{BMD$4ZS)V}qyZ4dxCp*D0?= zz04=&vG@nPxYk_3DIR;|i%@;+49Ako^A^mW_VM+e2e%IB-l`A@rExFNvfU&1?7W%>S8rLBL-tX!5icq00>##@c{-0F_PTXq z{$ab|*Z11@#_lcG{@u7!cjL;woA=Ca?#Y?Af7Y`@U(W30^u2BJIrrb*iddWa zohSeOmU$CbFD;cPS5Wu&p5E8Y`E9LcyLUh=mT^U0mH57#JIX;_5)G!SICPnR|eL zFi>m-kk13gq6}Q_!J$q-u`NLUQ6R0cXx}zb|6qM11_q`Ypja3PdtF>0m7JScUjP)m z0#qap#CN_$@7tbSR-(Yb!2AZNC<=rH*?6tf3es~K7+5@jYBmDZr{3J6zdSv$7^s)^ z4^R!nVrB;R^qk5x1_m|`1_rrE1_sk7E$3bCXQU>kFfgb9F@qTh|F-z~I|C>P6r01q zP|u(M6jb3CW;~gZTT%gJ?g8?511-|a6O|Xq$xj9#Dp)~woPf|wT?|2tUwD{VnHZTE*+HOz zLFvB~^Y8ymKucH{7I3%~rKSQ+Rrvp(&FvpBp4i+NcLDYDPGSa<3>9;FFZg>Mb`W9v zaDGW%SHQ3IK(DD^5+`SEU>DHs(zvQUB~Rrp$GrLl7uK?W6w=$(@lth{!NM2IY#q&Y zA5X75xV15UpZN#1&qfSh3^7)%*WzPjZ*1+}6g&OHL7VpDt9tm@Bocchx1BpQclXBY z0`6Ktlc)U}~0d<+(jS4Ojj&*;r}*w{}bQ z-MwpO-Aui9Ua#KBK0Ezv$^1I~`_s;sZTfHL{_}Od^Wk%gKmC7mv4Jl$B;)dq0-xYn z7ddmeV-I*{9K7}X>RHiqGd4boJ-9l!E{&sOalz3w8ZpMJLoQn`Sz2)TiR7XD$I~RW z?ijXneLvHownn}|A+RE4UFP9mFU3!Xdv`GWO9Cdh*{n9K`x$~5CX1>DZ5CJF$OuYP zOu%Sh`v0GSd2<6}q8vLgSM)sySUP!$g8t-wMTN Date: Thu, 20 Feb 2020 17:44:02 -0500 Subject: [PATCH 324/393] harcode dependency order in Makefile There are differences in the order that files get included when you compare the built file on linux and mac. I think the order that glob returns files differs between operating systems. We are now explicitly listing the files to avoid random differences. We only stumbled upon this because there is actually a bug that exists when mathquill is compiled on linux but doesn't exist when compiled on mac. We are accepting the order that mac uses. There's no guarantee that this is the definite best order. Only time will tell. --- Makefile | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/Makefile b/Makefile index cb8f96b3e..d492f2a15 100644 --- a/Makefile +++ b/Makefile @@ -40,17 +40,22 @@ BASE_SOURCES = \ $(SRC_DIR)/cursor.js \ $(SRC_DIR)/controller.js \ $(SRC_DIR)/publicapi.js \ - $(SRC_DIR)/services/*.util.js \ - $(SRC_DIR)/services/*.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 + SOURCES_FULL = \ $(BASE_SOURCES) \ $(SRC_DIR)/commands/math.js \ $(SRC_DIR)/commands/text.js \ $(SRC_DIR)/commands/math/*.js -# FIXME text.js currently depends on math.js (#435), restore these when fixed: -# $(SRC_DIR)/commands/*.js \ -# $(SRC_DIR)/commands/*/*.js SOURCES_BASIC = \ $(BASE_SOURCES) \ From 7fef30675bff771879fff0ac6a446ae96f1cf510 Mon Sep 17 00:00:00 2001 From: Eli Luberoff Date: Mon, 18 May 2020 16:51:33 -0700 Subject: [PATCH 325/393] get rid of bad border-radius rule not needed, and messes up other selectors in applications that use mathquill --- src/css/editable.less | 1 - 1 file changed, 1 deletion(-) diff --git a/src/css/editable.less b/src/css/editable.less index 52e154513..0a0fcd6c8 100644 --- a/src/css/editable.less +++ b/src/css/editable.less @@ -19,7 +19,6 @@ &.mq-focused { .box-shadow(~"#8bd 0 0 1px 2px, inset #6ae 0 0 2px 0"); border-color: #709AC0; - border-radius: 1px; aria-hidden: true; } } From 8007f9e165f169e63f52d7f548bcbdcd44f65fb3 Mon Sep 17 00:00:00 2001 From: Jason Merrill Date: Tue, 2 Jun 2020 15:52:51 -0700 Subject: [PATCH 326/393] Hide gray box in empty square brackets --- src/commands/math.js | 2 ++ src/css/math.less | 2 +- src/tree.js | 6 ++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/commands/math.js b/src/commands/math.js index f0a187a6d..64dd413e4 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -490,6 +490,8 @@ var MathBlock = P(MathElement, function(_, super_) { 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'); } } return this; diff --git a/src/css/math.less b/src/css/math.less index 81b983a83..427a79b34 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-parens, &.mq-empty-square-brackets { background: transparent } } diff --git a/src/tree.js b/src/tree.js index 9a06be21f..7732c99c3 100644 --- a/src/tree.js +++ b/src/tree.js @@ -265,6 +265,12 @@ var Node = P(function(_) { return this.parent.ctrlSeq === '\\left('; } + _.isEmptySquareBrackets = function () { + if (!this.isEmpty()) return false; + if (!this.parent) return false; + return this.parent.ctrlSeq === '\\left['; + } + _.isStyleBlock = function() { return false; }; From a5706c02508758e503136ff84fa30ac4809ec65d Mon Sep 17 00:00:00 2001 From: Mike Haverstock Date: Thu, 2 Jul 2020 14:41:16 -0400 Subject: [PATCH 327/393] add "make unminified_basic" target --- Makefile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index d492f2a15..ef2a23836 100644 --- a/Makefile +++ b/Makefile @@ -50,7 +50,7 @@ BASE_SOURCES = \ $(SRC_DIR)/services/mouse.js \ $(SRC_DIR)/services/scrollHoriz.js \ $(SRC_DIR)/services/textarea.js - + SOURCES_FULL = \ $(BASE_SOURCES) \ $(SRC_DIR)/commands/math.js \ @@ -111,6 +111,7 @@ BUILD_DIR_EXISTS = $(BUILD_DIR)/.exists--used_by_Makefile .PHONY: all basic dev js uglify css font clean all: font css uglify basic: $(UGLY_BASIC_JS) $(BASIC_CSS) +unminified_basic: $(BASIC_JS) $(BASIC_CSS) # dev is like all, but without minification dev: font css js js: $(BUILD_JS) From 84c8d3f986982c26cc54294d7c1b4641f6e821bf Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Mon, 23 Nov 2020 11:09:58 -0500 Subject: [PATCH 328/393] Expand VoiceOver iOS work-around to account for spoofed Mac user agent in iPad OS 13 + This fixes a problem where static math content wasn't rendering on iPads running iOS 13 and above. Detection taken adapted from Albany. --- src/services/textarea.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/services/textarea.js b/src/services/textarea.js index 355a611da..077001822 100644 --- a/src/services/textarea.js +++ b/src/services/textarea.js @@ -84,9 +84,11 @@ Controller.open(function(_) { // and omitting it makes the material available to Mac users. // For now, the solution is to render role="math" unless the user is on Mac. // Bug report: https://feedbackassistant.apple.com/feedback/7076111 + // Update: As of 11/23/2020, this problem becomes slightly more complicated now that iOS 13+ on iPads masquerades as a Mac. + // The same work-around applies, but now we must detect a spoofed Mac. var userAgent = navigator.userAgent || navigator.vendor || window.opera; - var isIOS = /iPad|iPhone|iPod/.test(userAgent) && !window.Stream; + var isIOS = (/iPad|iPhone|iPod/.test(userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) && !window.Stream; var isMac = navigator.appVersion.indexOf("Mac") !== -1 && !isIOS; if (!isMac) ctrlr.container.attr('role', 'math'); From b20f97172d184b5bf8ada41277e64b317195dcba Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Mon, 23 Nov 2020 14:18:09 -0500 Subject: [PATCH 329/393] Add missing time value to test suite tests --- test/unit.html | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/unit.html b/test/unit.html index c1fba135d..491d2c38e 100644 --- a/test/unit.html +++ b/test/unit.html @@ -111,9 +111,14 @@

Unit Tests

for (var suiteTitle in suiteMap) { if (suiteMap.hasOwnProperty(suiteTitle)) { var suiteResults = suiteMap[suiteTitle]; + var duration = 0; + suiteResults.assertions.forEach(function(assertion) { + duration += assertion.elapsedTime; + }); moduleResults.push({ name: suiteTitle, - assertions: suiteResults.assertions + assertions: suiteResults.assertions, + time: duration }); } } From bb0a95959f7bc21ea924d4a3a8219d18a848c85a Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Mon, 23 Nov 2020 14:32:54 -0500 Subject: [PATCH 330/393] Add StackOverflow post to spoofed Mac comments --- src/services/textarea.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/services/textarea.js b/src/services/textarea.js index 077001822..d31e8efd0 100644 --- a/src/services/textarea.js +++ b/src/services/textarea.js @@ -86,6 +86,7 @@ Controller.open(function(_) { // Bug report: https://feedbackassistant.apple.com/feedback/7076111 // Update: As of 11/23/2020, this problem becomes slightly more complicated now that iOS 13+ on iPads masquerades as a Mac. // The same work-around applies, but now we must detect a spoofed Mac. + // Technique based on https://stackoverflow.com/questions/57765958/how-to-detect-ipad-and-ipad-os-version-in-ios-13-and-up var userAgent = navigator.userAgent || navigator.vendor || window.opera; var isIOS = (/iPad|iPhone|iPod/.test(userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) && !window.Stream; From f5d4348bc08afec213f079fcc06d137a599860c0 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Fri, 12 Mar 2021 17:06:59 -0500 Subject: [PATCH 331/393] Use regular expression to separate digits for mathspeak The previous version added a space before the decimal point so that most screen readers would announce digits after the decimal individually (mostly a problem on the Mac). This worked for nearly every device except iOS devices. This addition replaces the previous hack with a regexp replace that splits out all contiguous digits after the decimal which should work everywhere. --- src/commands/math.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/commands/math.js b/src/commands/math.js index 64dd413e4..6b0174cc6 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -394,18 +394,18 @@ var MathBlock = P(MathElement, function(_, super_) { } var mathspeakText = cmd.mathspeak(); var cmdText = cmd.ctrlSeq; - if (isNaN(cmdText)) { - mathspeakText = ' ' + mathspeakText; - if (cmdText !== '.') { - mathspeakText += ' '; - } + if (isNaN(cmdText) && cmdText !== '.') { + mathspeakText = ' ' + mathspeakText + ' '; } speechArray.push(mathspeakText); } return speechArray; }) .join('') - .replace(/ +(?= )/g,''); + .replace(/ +(?= )/g,'') + .replace(/(\.)([0-9]+)/g, function(match, p1, p2) { + return p1 + p2.split('').join(' ').trim(); + }); }; _.ariaLabel = 'block'; From ae7b2aa957acf72c68beaa5cf5df8791faa6d1bb Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Fri, 12 Mar 2021 17:16:41 -0500 Subject: [PATCH 332/393] Add digit separation test --- test/unit/publicapi.test.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/test/unit/publicapi.test.js b/test/unit/publicapi.test.js index 8efca6e23..a82d43955 100644 --- a/test/unit/publicapi.test.js +++ b/test/unit/publicapi.test.js @@ -177,6 +177,9 @@ suite('Public API', function() { } } + mq.latex('123.456'); + assertMathSpeakEqual(mq.mathspeak(), '123.4 5 6'); + mq.latex('\\frac{d}{dx}\\sqrt{x}'); assertMathSpeakEqual(mq.mathspeak(), 'StartFraction "d" Over "d" "x" EndFraction StartRoot "x" EndRoot'); From 501dfea7896133017a63729c0c4eee42fc4baf1d Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Fri, 12 Mar 2021 17:25:52 -0500 Subject: [PATCH 333/393] Add explanatory comment --- src/commands/math.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/commands/math.js b/src/commands/math.js index 6b0174cc6..c35182383 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -403,6 +403,9 @@ var MathBlock = P(MathElement, function(_, super_) { }) .join('') .replace(/ +(?= )/g,'') + // 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) { return p1 + p2.split('').join(' ').trim(); }); From fda936b5e0fc24d168da093b1744bf4faba9dc75 Mon Sep 17 00:00:00 2001 From: Jason Merrill Date: Sat, 8 May 2021 18:51:15 -0700 Subject: [PATCH 334/393] Fix build on macOS Big Sur The build system has been using `sed -i` to do inline string replacement in files, but the `-i` option to sed is not POSIX and its behavior varies between GNU and BSD. The build system attempted to detect which system you were on by checking the behavior of a different flag (`-r`), but this detection no longer works correctly on macOS Big Sur because it has added the `-r` flag for compatibility with GNU (causing it to be incorrectly detected as GNU). Fix this by replacing `sed -i` with `perl -pi -e`. Perl is very widely available, and its behavior here is more standard across systems. Ref: * https://stackoverflow.com/questions/3156178/how-do-i-use-perl-like-sed/37686753#37686753 * https://stackoverflow.com/questions/5694228/sed-in-place-flag-that-works-both-on-mac-bsd-and-linux/22247781#22247781 Fixes https://github.com/mathquill/mathquill/issues/925 --- Makefile | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/Makefile b/Makefile index ef2a23836..59257c616 100644 --- a/Makefile +++ b/Makefile @@ -15,14 +15,6 @@ ifneq ($(shell node -e 'console.log("I am Node.js")'), I am Node.js) $(error Please install Node.js: https://nodejs.org/ ) endif -# stupid GNU vs BSD https://github.com/mathquill/mathquill/pull/653/commits/4332b0e97a92fb1362123a06b68fa49d9efb6f38#r68305423 -ifeq (x, $(shell echo xy | sed -r 's/(x)y/\1/' 2>/dev/null)) - SED_IN_PLACE = sed -i # GNU -else - SED_IN_PLACE = sed -i '' # BSD -endif - - # # -*- Configuration -*- # @@ -125,29 +117,29 @@ $(PJS_SRC): $(NODE_MODULES_INSTALLED) $(BUILD_JS): $(INTRO) $(SOURCES_FULL) $(OUTRO) $(BUILD_DIR_EXISTS) cat $^ | ./script/escape-non-ascii > $@ - $(SED_IN_PLACE) s/mq-/$(MQ_CLASS_PREFIX)mq-/g $@ - $(SED_IN_PLACE) s/{VERSION}/v$(VERSION)/ $@ + perl -pi -e s/mq-/$(MQ_CLASS_PREFIX)mq-/g $@ + perl -pi -e s/{VERSION}/v$(VERSION)/ $@ $(UGLY_JS): $(BUILD_JS) $(NODE_MODULES_INSTALLED) $(UGLIFY) $(UGLIFY_OPTS) < $< > $@ $(BASIC_JS): $(INTRO) $(SOURCES_BASIC) $(OUTRO) $(BUILD_DIR_EXISTS) cat $^ | ./script/escape-non-ascii > $@ - $(SED_IN_PLACE) s/mq-/$(MQ_CLASS_PREFIX)mq-/g $@ - $(SED_IN_PLACE) s/{VERSION}/v$(VERSION)/ $@ + perl -pi -e s/mq-/$(MQ_CLASS_PREFIX)mq-/g $@ + perl -pi -e s/{VERSION}/v$(VERSION)/ $@ $(UGLY_BASIC_JS): $(BASIC_JS) $(NODE_MODULES_INSTALLED) $(UGLIFY) $(UGLIFY_OPTS) < $< > $@ $(BUILD_CSS): $(CSS_SOURCES) $(NODE_MODULES_INSTALLED) $(BUILD_DIR_EXISTS) $(LESSC) $(LESS_OPTS) $(CSS_MAIN) > $@ - $(SED_IN_PLACE) s/mq-/$(MQ_CLASS_PREFIX)mq-/g $@ - $(SED_IN_PLACE) s/{VERSION}/v$(VERSION)/ $@ + perl -pi -e s/mq-/$(MQ_CLASS_PREFIX)mq-/g $@ + perl -pi -e s/{VERSION}/v$(VERSION)/ $@ $(BASIC_CSS): $(CSS_SOURCES) $(NODE_MODULES_INSTALLED) $(BUILD_DIR_EXISTS) $(LESSC) --modify-var="basic=true" $(LESS_OPTS) $(CSS_MAIN) > $@ - $(SED_IN_PLACE) s/mq-/$(MQ_CLASS_PREFIX)mq-/g $@ - $(SED_IN_PLACE) s/{VERSION}/v$(VERSION)/ $@ + perl -pi -e s/mq-/$(MQ_CLASS_PREFIX)mq-/g $@ + perl -pi -e s/{VERSION}/v$(VERSION)/ $@ $(NODE_MODULES_INSTALLED): package.json NODE_ENV=development npm install @@ -174,4 +166,4 @@ test: dev $(BUILD_TEST) $(BASIC_JS) $(BASIC_CSS) $(BUILD_TEST): $(INTRO) $(SOURCES_FULL) $(UNIT_TESTS) $(OUTRO) $(BUILD_DIR_EXISTS) cat $^ > $@ - $(SED_IN_PLACE) s/{VERSION}/v$(VERSION)/ $@ + perl -pi -e s/{VERSION}/v$(VERSION)/ $@ From de9faa98f3b03fcc8307738df032f38af4708982 Mon Sep 17 00:00:00 2001 From: Jason Merrill Date: Tue, 1 Jun 2021 14:50:31 -0700 Subject: [PATCH 335/393] Fix redundant focus event on paste Mathquill has a workaround for Linux that makes sure the appropriate element is focused on a paste event. This focus event bubbles in the usual way, so it may be observed outside mathquill. This extra focus event started tickling a bug in the Desmos graphing calculator after upgrading from jQuery 2.x to 3.x. I suspect there may be a difference between these versions (possibly only in some browsers) in handling the case that the appropriate element already had focus. Switch to only triggering this event if the appropriate element is not already focused to reduce the chance of triggering focus related application bugs. --- src/services/saneKeyboardEvents.util.js | 4 +++- test/unit/saneKeyboardEvents.test.js | 21 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/services/saneKeyboardEvents.util.js b/src/services/saneKeyboardEvents.util.js index 93a61a691..57dba0f92 100644 --- a/src/services/saneKeyboardEvents.util.js +++ b/src/services/saneKeyboardEvents.util.js @@ -281,7 +281,9 @@ var saneKeyboardEvents = (function() { // on keydown too, FWIW). // // And by nifty, we mean dumb (but useful sometimes). - textarea.focus(); + if (document.activeElement !== textarea[0]) { + textarea.focus(); + } checkTextareaFor(pastedText); } diff --git a/test/unit/saneKeyboardEvents.test.js b/test/unit/saneKeyboardEvents.test.js index 0f024df04..198464f71 100644 --- a/test/unit/saneKeyboardEvents.test.js +++ b/test/unit/saneKeyboardEvents.test.js @@ -426,6 +426,27 @@ suite('saneKeyboardEvents', function() { // before the paste timeout happens. el.trigger('input'); }); + + test('pasting into a focused textarea should not fire a redundant focus event', function(done) { + el.focus(); + + var focusCalled = false; + el.focus(function () { + focusCalled = true; + }); + + saneKeyboardEvents(el, { + paste: function () { + assert.ok(!focusCalled, 'Pasting into a focused mathquill should not fire a focus event'); + done(); + } + }); + + // Simulate a paste + el.trigger('paste'); + el.val('2'); + el.trigger('input'); + }); }); suite('copy', function() { From 2ac67b48f50c0ecb3893b5f5829313e72ff46a64 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Wed, 2 Jun 2021 17:02:29 -0400 Subject: [PATCH 336/393] Force an ARIA update for static math when its LaTeX changes --- src/commands/math.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/commands/math.js b/src/commands/math.js index c35182383..ff4f395e2 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -525,6 +525,8 @@ API.StaticMath = function(APIClasses) { this.__controller.root.postOrder(function (node) { node.registerInnerField(innerFields, APIClasses.MathField); }); + // Force an ARIA label update to remain in sync with the new LaTeX value. + this.setAriaLabel(this.__controller.ariaLabel); } return returned; }; From f0a9c49a72a17d04a894806a88ebd0deca13462b Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Mon, 19 Jul 2021 14:25:06 -0400 Subject: [PATCH 337/393] Simplify exponent speech for common whole numbers Currently handles 1 through 3. Anything else returns the existing Superscript/Baseline value as before. --- src/commands/math/commands.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 469fd7648..7f4049620 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -342,7 +342,21 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { + '
' ; _.textTemplate = [ '^' ]; - _.mathspeakTemplate = [ 'Superscript,', ', Baseline']; + _.mathspeak = function() { + // Simplify basic exponent speech for common whole numbers. + var innerMathspeak = this.ends[L] && this.ends[L].mathspeak(); + if (innerMathspeak === undefined) { + return ''; + } else if (innerMathspeak === '1') { + return 'to the first power'; + } else if (innerMathspeak === '2') { + return 'squared'; + } else if (innerMathspeak === '3') { + return 'cubed'; + } + return 'Superscript, ' + innerMathspeak + ', Baseline'; + }; + _.ariaLabel = 'superscript'; _.finalizeTree = function() { this.upInto = this.sup = this.ends[R]; From 46ef623b15c80b71445b393f426a910c8b3a906e Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Mon, 19 Jul 2021 15:00:58 -0400 Subject: [PATCH 338/393] Simplify whole-number fractions whose denominators < 10 Note: purposefully excludes 1 and 0. --- src/commands/math/commands.js | 59 +++++++++++++++++++++++++++++++++-- 1 file changed, 56 insertions(+), 3 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 7f4049620..dcefb0eed 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -475,15 +475,68 @@ LatexCmds.fraction = P(MathCommand, function(_, super_) { this.downInto = this.ends[L].downOutOf = this.ends[R]; this.ends[L].ariaLabel = 'numerator'; this.ends[R].ariaLabel = 'denominator'; - if(this.getFracDepth() > 1) this.mathspeakTemplate = ['StartNestedFraction,', 'NestedOver', ', EndNestedFraction']; - else this.mathspeakTemplate = ['StartFraction,', 'Over', ', EndFraction']; }; _.mathspeak = function(opts) { if (opts && opts.createdLeftOf) { var cursor = opts.createdLeftOf; return cursor.parent.mathspeak(); } - return super_.mathspeak.apply(this, arguments); + + var numeratorMathspeak = this.ends[L].mathspeak(); + var denominatorMathspeak = this.ends[R].mathspeak(); + + // Shorten mathspeak value for whole number fractions whose denominator is less than 10. + if ( + !isNaN(numeratorMathspeak) && + !isNaN(denominatorMathspeak) && + numeratorMathspeak === parseInt(numeratorMathspeak, 10).toString() && + denominatorMathspeak === parseInt(denominatorMathspeak, 10).toString() + ) { + var denominatorText = ''; + if (denominatorMathspeak === '2') { + denominatorText = numeratorMathspeak === '1' + ? 'half' + : 'halves'; + } else if (denominatorMathspeak === '3') { + denominatorText = numeratorMathspeak === '1' + ? 'third' + : 'thirds'; + } else if (denominatorMathspeak === '4') { + denominatorText = numeratorMathspeak === '1' + ? 'quarter' + : 'quarters'; + } else if (denominatorMathspeak === '5') { + denominatorText = numeratorMathspeak === '1' + ? 'fifth' + : 'fifths'; + } else if (denominatorMathspeak === '6') { + denominatorText = numeratorMathspeak === '1' + ? 'sixth' + : 'sixths'; + } else if (denominatorMathspeak === '7') { + denominatorText = numeratorMathspeak === '1' + ? 'seventh' + : 'sevenths'; + } else if (denominatorMathspeak === '8') { + denominatorText = numeratorMathspeak === '1' + ? 'eighth' + : 'eighths'; + } else if (denominatorMathspeak === '9') { + denominatorText = numeratorMathspeak === '1' + ? 'ninth' + : 'ninths'; + } + if (denominatorText !== '') { + return numeratorMathspeak + ' ' + denominatorText; + } + } + + var depth = this.getFracDepth(); + if(depth > 1) { + return 'StartNestedFraction, ' + numeratorMathspeak + ' NestedOver ' + denominatorMathspeak + ', EndNestedFraction'; + } else { + return 'StartFraction, ' + numeratorMathspeak + ' Over ' + denominatorMathspeak + ', EndFraction'; + } }; _.getFracDepth = function() { From c9604846fe0afeee90e8f95b5aee0731d994c277 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Mon, 19 Jul 2021 16:21:41 -0400 Subject: [PATCH 339/393] Improve voicing of simple mixed fractions For example, instead of 1 1 half, speak 1 and 1 half. --- src/commands/math.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/commands/math.js b/src/commands/math.js index ff4f395e2..4dbe7f224 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -379,6 +379,7 @@ var MathBlock = P(MathElement, function(_, super_) { var tempOp = ''; var autoOps = {}; if (this.controller) autoOps = this.controller.options.autoOperatorNames; + var wasPrevNumeric = false; return this.foldChildren([], function(speechArray, cmd) { if (cmd.isPartOfOperator) { tempOp += cmd.mathspeak(); @@ -394,10 +395,23 @@ var MathBlock = P(MathElement, function(_, super_) { } var mathspeakText = cmd.mathspeak(); var cmdText = cmd.ctrlSeq; - if (isNaN(cmdText) && cmdText !== '.') { + var isCmdNumeric = !isNaN(cmdText); + + // Handle the case of a digit followed by a simplified fraction such as 1\frac{1}{2}. + // Such combinations should be spoken aloud as "1 and 1 half." + if ( + wasPrevNumeric && + cmdText.indexOf('frac') !== -1 && + mathspeakText.indexOf('Fraction') === -1 + ) { + speechArray.push('and'); + } + + if (!isCmdNumeric && cmdText !== '.') { mathspeakText = ' ' + mathspeakText + ' '; } speechArray.push(mathspeakText); + wasPrevNumeric = isCmdNumeric; } return speechArray; }) From a34de1ee0a6de0cb49c8c1c64913cd2bd9d8ad4c Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 20 Jul 2021 13:53:51 -0400 Subject: [PATCH 340/393] Account for whitespace when computing mixed fraction speech --- src/commands/math.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/commands/math.js b/src/commands/math.js index 4dbe7f224..4b6bf6f53 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -383,8 +383,7 @@ var MathBlock = P(MathElement, function(_, super_) { return this.foldChildren([], function(speechArray, cmd) { if (cmd.isPartOfOperator) { tempOp += cmd.mathspeak(); - } - else { + } else { if(tempOp!=='') { if(autoOps !== {} && autoOps._maxLength > 0) { var x = autoOps[tempOp.toLowerCase()]; @@ -395,23 +394,27 @@ var MathBlock = P(MathElement, function(_, super_) { } var mathspeakText = cmd.mathspeak(); var cmdText = cmd.ctrlSeq; - var isCmdNumeric = !isNaN(cmdText); + var isCmdNumeric = /^\d$/.test(cmdText); // Handle the case of a digit followed by a simplified fraction such as 1\frac{1}{2}. // Such combinations should be spoken aloud as "1 and 1 half." if ( wasPrevNumeric && cmdText.indexOf('frac') !== -1 && - mathspeakText.indexOf('Fraction') === -1 + mathspeakText.indexOf('Fraction') === -1 ) { - speechArray.push('and'); + speechArray.push(' and '); } if (!isCmdNumeric && cmdText !== '.') { mathspeakText = ' ' + mathspeakText + ' '; } speechArray.push(mathspeakText); - wasPrevNumeric = isCmdNumeric; + // Update wasPrevNumeric ignoring whitespace + // e.g. We want to make 1\frac{1}{2} equivalent speech-wise to 1 \frac{1}{2}. + if (cmdText !== '\\ ') { + wasPrevNumeric = isCmdNumeric; + } } return speechArray; }) From 27c67b7b202090f7d85702e1b40747e64c31659c Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 20 Jul 2021 14:41:12 -0400 Subject: [PATCH 341/393] Allow empty superscripts to be reported again --- src/commands/math/commands.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index dcefb0eed..f8881ec08 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -344,10 +344,8 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { _.textTemplate = [ '^' ]; _.mathspeak = function() { // Simplify basic exponent speech for common whole numbers. - var innerMathspeak = this.ends[L] && this.ends[L].mathspeak(); - if (innerMathspeak === undefined) { - return ''; - } else if (innerMathspeak === '1') { + var innerMathspeak = (this.ends[L] && this.ends[L].mathspeak()) || ''; + if (innerMathspeak === '1') { return 'to the first power'; } else if (innerMathspeak === '2') { return 'squared'; From 4415d74e883baf7746f7d779c1dd8dd9652e69bf Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 20 Jul 2021 15:03:56 -0400 Subject: [PATCH 342/393] Use regEx for shortened fraction speech computation --- src/commands/math/commands.js | 54 ++++++++++++++++------------------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index f8881ec08..910a76e11 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -480,60 +480,56 @@ LatexCmds.fraction = P(MathCommand, function(_, super_) { return cursor.parent.mathspeak(); } - var numeratorMathspeak = this.ends[L].mathspeak(); - var denominatorMathspeak = this.ends[R].mathspeak(); + var numSpeech = this.ends[L].mathspeak(); + var denSpeech = this.ends[R].mathspeak(); // Shorten mathspeak value for whole number fractions whose denominator is less than 10. - if ( - !isNaN(numeratorMathspeak) && - !isNaN(denominatorMathspeak) && - numeratorMathspeak === parseInt(numeratorMathspeak, 10).toString() && - denominatorMathspeak === parseInt(denominatorMathspeak, 10).toString() - ) { - var denominatorText = ''; - if (denominatorMathspeak === '2') { - denominatorText = numeratorMathspeak === '1' + var intRgx = new RegExp(/^[\d]+$/); + if (intRgx.test(numSpeech) && intRgx.test(denSpeech)) { + var newDenSpeech = ''; + if (denSpeech === '2') { + newDenSpeech = numSpeech === '1' ? 'half' : 'halves'; - } else if (denominatorMathspeak === '3') { - denominatorText = numeratorMathspeak === '1' + } else if (denSpeech === '3') { + newDenSpeech = numSpeech === '1' ? 'third' : 'thirds'; - } else if (denominatorMathspeak === '4') { - denominatorText = numeratorMathspeak === '1' + } else if (denSpeech === '4') { + newDenSpeech = numSpeech === '1' ? 'quarter' : 'quarters'; - } else if (denominatorMathspeak === '5') { - denominatorText = numeratorMathspeak === '1' + } else if (denSpeech === '5') { + newDenSpeech = numSpeech === '1' ? 'fifth' : 'fifths'; - } else if (denominatorMathspeak === '6') { - denominatorText = numeratorMathspeak === '1' + } else if (denSpeech === '6') { + newDenSpeech = numSpeech === '1' ? 'sixth' : 'sixths'; - } else if (denominatorMathspeak === '7') { - denominatorText = numeratorMathspeak === '1' + } else if (denSpeech === '7') { + newDenSpeech = numSpeech === '1' ? 'seventh' : 'sevenths'; - } else if (denominatorMathspeak === '8') { - denominatorText = numeratorMathspeak === '1' + } else if (denSpeech === '8') { + newDenSpeech = numSpeech === '1' ? 'eighth' : 'eighths'; - } else if (denominatorMathspeak === '9') { - denominatorText = numeratorMathspeak === '1' + } else if (denSpeech === '9') { + newDenSpeech = numSpeech === '1' ? 'ninth' : 'ninths'; } - if (denominatorText !== '') { - return numeratorMathspeak + ' ' + denominatorText; + if (newDenSpeech !== '') { + return numSpeech + ' ' + newDenSpeech; } } var depth = this.getFracDepth(); if(depth > 1) { - return 'StartNestedFraction, ' + numeratorMathspeak + ' NestedOver ' + denominatorMathspeak + ', EndNestedFraction'; + return 'StartNestedFraction, ' + numSpeech + ' NestedOver ' + denSpeech + ', EndNestedFraction'; } else { - return 'StartFraction, ' + numeratorMathspeak + ' Over ' + denominatorMathspeak + ', EndFraction'; + return 'StartFraction, ' + numSpeech + ' Over ' + denSpeech + ', EndFraction'; } }; From e829952d7579281b0185dcba8dbc2f422bff9db2 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 20 Jul 2021 15:08:49 -0400 Subject: [PATCH 343/393] Change numeric test regexp in block.foldChildren function --- src/commands/math.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/math.js b/src/commands/math.js index 4b6bf6f53..2ce335db3 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -394,7 +394,7 @@ var MathBlock = P(MathElement, function(_, super_) { } var mathspeakText = cmd.mathspeak(); var cmdText = cmd.ctrlSeq; - var isCmdNumeric = /^\d$/.test(cmdText); + var isCmdNumeric = /^[\d]+$/.test(cmdText); // Handle the case of a digit followed by a simplified fraction such as 1\frac{1}{2}. // Such combinations should be spoken aloud as "1 and 1 half." From 2b5739138d50c08cc052a3765a87579bbe095bf4 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Wed, 21 Jul 2021 10:07:04 -0400 Subject: [PATCH 344/393] Distinguish 'minus' and 'negative' speech e.g. the - inside 1-3 is read as minus, and -1 is read as negative. --- src/commands/math/basicSymbols.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index 57078a0a8..6ce54ff5d 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -819,7 +819,15 @@ var PlusMinus = P(BinaryOperator, function(_) { LatexCmds['+'] = bind(PlusMinus, '+', '+', 'plus'); //yes, these are different dashes, en-dash, em-dash, unicode minus, actual dash -LatexCmds['−'] = LatexCmds['—'] = LatexCmds['–'] = LatexCmds['-'] = bind(PlusMinus, '-', '−', 'minus'); +LatexCmds['−'] = LatexCmds['—'] = LatexCmds['–'] = LatexCmds['-'] = P(PlusMinus, function(_, super_) { + _.init = function () { + super_.init.call(this, '-', '−'); + }; + _.mathspeak = function() { + return this.jQ[0].className === 'mq-binary-operator' ? 'minus' : 'negative'; + }; +}); + LatexCmds['±'] = LatexCmds.pm = LatexCmds.plusmn = LatexCmds.plusminus = bind(PlusMinus,'\\pm ','±', 'plus-or-minus'); LatexCmds.mp = LatexCmds.mnplus = LatexCmds.minusplus = From 52dd856b168aa1975921d6dc11b7e56c6525e93b Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Wed, 21 Jul 2021 12:15:51 -0400 Subject: [PATCH 345/393] Add more robust superscript shortening --- src/commands/math/commands.js | 48 ++++++++++++++++++++++++++++------- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 910a76e11..63f1197e2 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -165,6 +165,10 @@ var Class = LatexCmds['class'] = P(MathCommand, function(_, super_) { }; }); +// 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. +var intRgx = new RegExp(/^[\+\-]?[\d]+$/); + var SupSub = P(MathCommand, function(_, super_) { _.ctrlSeq = '_{...}^{...}'; _.createLeftOf = function(cursor) { @@ -344,18 +348,45 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { _.textTemplate = [ '^' ]; _.mathspeak = function() { // Simplify basic exponent speech for common whole numbers. - var innerMathspeak = (this.ends[L] && this.ends[L].mathspeak()) || ''; - if (innerMathspeak === '1') { - return 'to the first power'; - } else if (innerMathspeak === '2') { - return 'squared'; - } else if (innerMathspeak === '3') { - return 'cubed'; + var innerText = (this.ends[L] && this.ends[L].text()); + // If the superscript is a whole number, shorten the speech that is returned. + if (intRgx.test(innerText)) { + // Simple cases + if (innerText === '1') { + return 'to the first power'; + } else if (innerText === '2') { + return 'squared'; + } else if (innerText === '3') { + return 'cubed'; + } + + // More complex cases. + var isNegative = innerText.startsWith('-'); + var isParentDigit = + this.parent && + this.parent.ends[L] && + this.parent.ends[L] instanceof Digit; + // If the superscript is negative and the parent is not a numeric literal, + // play it safe and don't shorten the mathspeak. + // For example, we don't know if a negative superscript is meant to signify exactly that + // or if it is being written to indicate an inverse function. + if (!isNegative || isParentDigit) { + var suffix = ''; + if (/(11|12|13|4|5|6|7|8|9|0)$/.test(innerText)) { + suffix = 'th'; + } else if (/2$/.test(innerText)) { + suffix = 'nd'; + } else if (/3$/.test(innerText)) { + suffix = 'rd'; + } + return 'to the ' + this.ends[L].mathspeak() + suffix + ' power'; + } } - return 'Superscript, ' + innerMathspeak + ', Baseline'; + return super_.mathspeak.call(this); }; _.ariaLabel = 'superscript'; + _.mathspeakTemplate = [ 'Superscript,', ', Baseline']; _.finalizeTree = function() { this.upInto = this.sup = this.ends[R]; this.sup.downOutOf = insLeftOfMeUnlessAtEnd; @@ -484,7 +515,6 @@ LatexCmds.fraction = P(MathCommand, function(_, super_) { var denSpeech = this.ends[R].mathspeak(); // Shorten mathspeak value for whole number fractions whose denominator is less than 10. - var intRgx = new RegExp(/^[\d]+$/); if (intRgx.test(numSpeech) && intRgx.test(denSpeech)) { var newDenSpeech = ''; if (denSpeech === '2') { From 04f36c3bfc29bd35b25f7cf8db3d3de22e289977 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Wed, 21 Jul 2021 14:22:15 -0400 Subject: [PATCH 346/393] Never use shorthand speech when issuing ARIA alerts Alerts are fired only when the user is editing an expression. For these cases, it is vital to ensure the technical form of part of an expression is spoken to avoid ambiguity. For instance, Tabbing out of a superscript of 2 results in Mathquill saying 'out of superscript, 2, baseline' instead of 'out of squared.' --- src/commands/math/commands.js | 12 +++++++++--- src/services/aria.js | 26 ++++++++++++++++++++------ 2 files changed, 29 insertions(+), 9 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 63f1197e2..53ce722d8 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -346,11 +346,14 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { + '
' ; _.textTemplate = [ '^' ]; - _.mathspeak = function() { + _.mathspeak = function(opts) { // Simplify basic exponent speech for common whole numbers. var innerText = (this.ends[L] && this.ends[L].text()); // If the superscript is a whole number, shorten the speech that is returned. - if (intRgx.test(innerText)) { + if ( + (!opts || !opts.ignoreShorthand) && + intRgx.test(innerText) + ) { // Simple cases if (innerText === '1') { return 'to the first power'; @@ -515,7 +518,10 @@ LatexCmds.fraction = P(MathCommand, function(_, super_) { var denSpeech = this.ends[R].mathspeak(); // Shorten mathspeak value for whole number fractions whose denominator is less than 10. - if (intRgx.test(numSpeech) && intRgx.test(denSpeech)) { + if ( + (!opts || !opts.ignoreShorthand) && + intRgx.test(numSpeech) && intRgx.test(denSpeech) + ) { var newDenSpeech = ''; if (denSpeech === '2') { newDenSpeech = numSpeech === '1' diff --git a/src/services/aria.js b/src/services/aria.js index 9609d17db..ea7f43096 100755 --- a/src/services/aria.js +++ b/src/services/aria.js @@ -26,16 +26,30 @@ var Aria = P(function(_) { }; _.queue = function(item, shouldDescribe) { + var output = ''; if (item instanceof Node) { + // 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. + var itemMathspeak = item.mathspeak({ignoreShorthand: true}); if (shouldDescribe) { // used to ensure item is described when cursor reaches block boundaries - if (item.parent && item.parent.ariaLabel && item.ariaLabel === 'block') item = item.parent.ariaLabel+' '+item.mathspeak(); - else if (item.ariaLabel) item = item.ariaLabel+' '+item.mathspeak(); - else item = item.mathspeak(); + if ( + item.parent && + item.parent.ariaLabel && + item.ariaLabel === 'block' + ) { + output = item.parent.ariaLabel+' '+itemMathspeak; + } else if (item.ariaLabel) { + output = item.ariaLabel+' '+itemMathspeak; + } } - else item = item.mathspeak(); - + if (output === '') { + output = itemMathspeak; + } + } else { + output = item; } - this.items.push(item); + this.items.push(output); return this; }; _.queueDirOf = function(dir) { From 20387bcee25f9bb99f4bbdea0c0487cd3f4f59e0 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Thu, 22 Jul 2021 10:37:50 -0400 Subject: [PATCH 347/393] Add missing 1st suffix check for shortened exponents --- src/commands/math/commands.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 53ce722d8..7e920db47 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -355,8 +355,8 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { intRgx.test(innerText) ) { // Simple cases - if (innerText === '1') { - return 'to the first power'; + if (innerText === '0') { + return 'to the 0 power'; } else if (innerText === '2') { return 'squared'; } else if (innerText === '3') { @@ -377,6 +377,8 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { var suffix = ''; if (/(11|12|13|4|5|6|7|8|9|0)$/.test(innerText)) { suffix = 'th'; + } else if (/1$/.test(innerText)) { + suffix = 'st'; } else if (/2$/.test(innerText)) { suffix = 'nd'; } else if (/3$/.test(innerText)) { From dcfb81b1b47f4f914583509d288ef437fe01eb83 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Thu, 22 Jul 2021 10:41:10 -0400 Subject: [PATCH 348/393] Add positive/plus dictinction to mathspeak Same logic as negative/minus. --- src/commands/math/basicSymbols.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index 6ce54ff5d..76f6e62fe 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -817,7 +817,15 @@ var PlusMinus = P(BinaryOperator, function(_) { }; }); -LatexCmds['+'] = bind(PlusMinus, '+', '+', 'plus'); +LatexCmds['+'] = P(PlusMinus, function(_, super_) { + _.init = function () { + super_.init.call(this, '+', '+'); + }; + _.mathspeak = function() { + return this.jQ[0].className === 'mq-binary-operator' ? '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 () { From 0b7223c272c3931fb53e41c8f1a8150ace530c12 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Thu, 22 Jul 2021 12:25:01 -0400 Subject: [PATCH 349/393] Limit exponent suffix addition to absolute values < 1000 From feedback, adding the suffix for large numbers is not as meaningful, especially when using Mac VoiceOver. --- src/commands/math/commands.js | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 7e920db47..dc842c6ad 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -375,14 +375,17 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { // or if it is being written to indicate an inverse function. if (!isNegative || isParentDigit) { var suffix = ''; - if (/(11|12|13|4|5|6|7|8|9|0)$/.test(innerText)) { - suffix = 'th'; - } else if (/1$/.test(innerText)) { - suffix = 'st'; - } else if (/2$/.test(innerText)) { - suffix = 'nd'; - } else if (/3$/.test(innerText)) { - suffix = 'rd'; + // Limit suffix addition to exponents < 1000. + if (Math.abs(innerText) < 1000) { + if (/(11|12|13|4|5|6|7|8|9|0)$/.test(innerText)) { + suffix = 'th'; + } else if (/1$/.test(innerText)) { + suffix = 'st'; + } else if (/2$/.test(innerText)) { + suffix = 'nd'; + } else if (/3$/.test(innerText)) { + suffix = 'rd'; + } } return 'to the ' + this.ends[L].mathspeak() + suffix + ' power'; } From 8bf8f66ed369d8a8bf36e11b3f658a65776e496c Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Thu, 22 Jul 2021 14:49:23 -0400 Subject: [PATCH 350/393] Tidy up some typing errors found in tests --- src/commands/math/commands.js | 85 ++++++++++++++++++++--------------- 1 file changed, 48 insertions(+), 37 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index dc842c6ad..f0ada02ef 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -348,46 +348,57 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { _.textTemplate = [ '^' ]; _.mathspeak = function(opts) { // Simplify basic exponent speech for common whole numbers. - var innerText = (this.ends[L] && this.ends[L].text()); - // If the superscript is a whole number, shorten the speech that is returned. - if ( - (!opts || !opts.ignoreShorthand) && - intRgx.test(innerText) - ) { - // Simple cases - if (innerText === '0') { - return 'to the 0 power'; - } else if (innerText === '2') { - return 'squared'; - } else if (innerText === '3') { - return 'cubed'; - } + var child = this.ends[L] || this.ends[R]; + if (child !== undefined) { + // Calculate this item's inner text to determine whether to shorten the returned speech. + // Do not calculate its inner mathspeak now until we know that the speech is to be truncated. + // Since the mathspeak computation is recursive, we want to call it only once in this function to avoid performance bottlenecks. + var innerText = typeof(child) === 'Object' + ? child.text() + : child; + // If the superscript is a whole number, shorten the speech that is returned. + if ( + (!opts || !opts.ignoreShorthand) && + intRgx.test(innerText) + ) { + // Simple cases + if (innerText === '0') { + return 'to the 0 power'; + } else if (innerText === '2') { + return 'squared'; + } else if (innerText === '3') { + return 'cubed'; + } - // More complex cases. - var isNegative = innerText.startsWith('-'); - var isParentDigit = - this.parent && - this.parent.ends[L] && - this.parent.ends[L] instanceof Digit; - // If the superscript is negative and the parent is not a numeric literal, - // play it safe and don't shorten the mathspeak. - // For example, we don't know if a negative superscript is meant to signify exactly that - // or if it is being written to indicate an inverse function. - if (!isNegative || isParentDigit) { - var suffix = ''; - // Limit suffix addition to exponents < 1000. - if (Math.abs(innerText) < 1000) { - if (/(11|12|13|4|5|6|7|8|9|0)$/.test(innerText)) { - suffix = 'th'; - } else if (/1$/.test(innerText)) { - suffix = 'st'; - } else if (/2$/.test(innerText)) { - suffix = 'nd'; - } else if (/3$/.test(innerText)) { - suffix = 'rd'; + // More complex cases. + var isNegative = /$&\-/.test(innerText); + var isParentDigit = + this.parent && + this.parent.ends[L] && + this.parent.ends[L] instanceof Digit; + // If the superscript is negative and the parent is not a numeric literal, + // play it safe and don't shorten the mathspeak. + // For example, we don't know if a negative superscript is meant to signify exactly that + // or if it is being written to indicate an inverse function. + if (!isNegative || isParentDigit) { + var suffix = ''; + // Limit suffix addition to exponents < 1000. + if (Math.abs(innerText) < 1000) { + if (/(11|12|13|4|5|6|7|8|9|0)$/.test(innerText)) { + suffix = 'th'; + } else if (/1$/.test(innerText)) { + suffix = 'st'; + } else if (/2$/.test(innerText)) { + suffix = 'nd'; + } else if (/3$/.test(innerText)) { + suffix = 'rd'; + } } + var innerMathspeak = typeof(child) === 'Object' + ? child.mathspeak() + : child; + return 'to the ' + innerMathspeak + suffix + ' power'; } - return 'to the ' + this.ends[L].mathspeak() + suffix + ' power'; } } return super_.mathspeak.call(this); From 1469d4fe94bc1ff5d005952c2499a655e25a587f Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Thu, 22 Jul 2021 15:12:35 -0400 Subject: [PATCH 351/393] Revert some fraction-related changes In particular, restore the prior mathspeak fallback behavior and template generation so that backspacing inside fractions works properly again. --- src/commands/math/commands.js | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index f0ada02ef..669f3d332 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -523,7 +523,13 @@ LatexCmds.fraction = P(MathCommand, function(_, super_) { this.downInto = this.ends[L].downOutOf = this.ends[R]; this.ends[L].ariaLabel = 'numerator'; this.ends[R].ariaLabel = 'denominator'; + if(this.getFracDepth() > 1) { + this.mathspeakTemplate = ['StartNestedFraction,', 'NestedOver', ', EndNestedFraction']; + } else { + this.mathspeakTemplate = ['StartFraction,', 'Over', ', EndFraction']; + } }; + _.mathspeak = function(opts) { if (opts && opts.createdLeftOf) { var cursor = opts.createdLeftOf; @@ -577,12 +583,7 @@ LatexCmds.fraction = P(MathCommand, function(_, super_) { } } - var depth = this.getFracDepth(); - if(depth > 1) { - return 'StartNestedFraction, ' + numSpeech + ' NestedOver ' + denSpeech + ', EndNestedFraction'; - } else { - return 'StartFraction, ' + numSpeech + ' Over ' + denSpeech + ', EndFraction'; - } + return super_.mathspeak.apply(this, arguments); }; _.getFracDepth = function() { From b742efc5030398540b533afa9cb8d250b3630e43 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Thu, 22 Jul 2021 17:56:08 -0400 Subject: [PATCH 352/393] Add tests and fix superscripts --- src/commands/math/commands.js | 10 +-- test/unit/publicapi.test.js | 4 +- test/unit/typing.test.js | 135 ++++++++++++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 7 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 669f3d332..eb7d13205 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -353,9 +353,9 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { // Calculate this item's inner text to determine whether to shorten the returned speech. // Do not calculate its inner mathspeak now until we know that the speech is to be truncated. // Since the mathspeak computation is recursive, we want to call it only once in this function to avoid performance bottlenecks. - var innerText = typeof(child) === 'Object' + var innerText = typeof(child) === 'object' ? child.text() - : child; + : ''+child; // If the superscript is a whole number, shorten the speech that is returned. if ( (!opts || !opts.ignoreShorthand) && @@ -371,7 +371,7 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { } // More complex cases. - var isNegative = /$&\-/.test(innerText); + var isNegative = /^(-)/.test(innerText); var isParentDigit = this.parent && this.parent.ends[L] && @@ -394,9 +394,9 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { suffix = 'rd'; } } - var innerMathspeak = typeof(child) === 'Object' + var innerMathspeak = typeof(child) === 'object' ? child.mathspeak() - : child; + : innerText; return 'to the ' + innerMathspeak + suffix + ' power'; } } diff --git a/test/unit/publicapi.test.js b/test/unit/publicapi.test.js index a82d43955..06ec80d31 100644 --- a/test/unit/publicapi.test.js +++ b/test/unit/publicapi.test.js @@ -184,11 +184,11 @@ suite('Public API', function() { assertMathSpeakEqual(mq.mathspeak(), 'StartFraction "d" Over "d" "x" EndFraction StartRoot "x" EndRoot'); mq.latex('1+2-3\\cdot\\frac{5}{6^7}=\\left(8+9\\right)'); - assertMathSpeakEqual(mq.mathspeak(), '1 plus 2 minus 3 times StartFraction 5 Over 6 Superscript 7 Baseline EndFraction equals left parenthesis 8 plus 9 right parenthesis'); + assertMathSpeakEqual(mq.mathspeak(), '1 plus 2 minus 3 times StartFraction 5 Over 6 to the 7 th power EndFraction equals left parenthesis 8 plus 9 right parenthesis'); // Example 13 from http://www.gh-mathspeak.com/examples/quick-tutorial/index.php?verbosity=v&explicitness=2&interp=0 mq.latex('d=\\sqrt{ \\left( x_2 - x_1 \\right)^2 - \\left( y_2 - y_1 \\right)^2 }'); - assertMathSpeakEqual(mq.mathspeak(), '"d" equals StartRoot left parenthesis "x" Subscript 2 Baseline minus "x" Subscript 1 Baseline right parenthesis Superscript 2 Baseline minus left parenthesis "y" Subscript 2 Baseline minus "y" Subscript 1 Baseline right parenthesis Superscript 2 Baseline EndRoot'); + assertMathSpeakEqual(mq.mathspeak(), '"d" equals StartRoot left parenthesis "x" Subscript 2 Baseline minus "x" Subscript 1 Baseline right parenthesis squared minus left parenthesis "y" Subscript 2 Baseline minus "y" Subscript 1 Baseline right parenthesis squared EndRoot'); mq.latex('').typedText('\\langle').keystroke('Spacebar').typedText('u,v'); // .latex() doesn't work yet for angle brackets :( assertMathSpeakEqual(mq.mathspeak(), 'left angle-bracket "u" "v" right angle-bracket'); diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index f8f951d82..b8d39d059 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -75,6 +75,141 @@ suite('typing with auto-replaces', function() { }); }); + suite('MathspeakShorthand', function() { + test('fractions', function() { + // Testing singular numeric fractions from 1/2 to 1/10 + mq.latex('\\frac{1}{2}'); + assertMathspeak('1 half'); + mq.latex('\\frac{1}{3}'); + assertMathspeak('1 third'); + mq.latex('\\frac{1}{4}'); + assertMathspeak('1 quarter'); + mq.latex('\\frac{1}{5}'); + assertMathspeak('1 fifth'); + mq.latex('\\frac{1}{6}'); + assertMathspeak('1 sixth'); + mq.latex('\\frac{1}{7}'); + assertMathspeak('1 seventh'); + mq.latex('\\frac{1}{8}'); + assertMathspeak('1 eighth'); + mq.latex('\\frac{1}{9}'); + assertMathspeak('1 ninth'); + mq.latex('\\frac{1}{10}'); + assertMathspeak('StartFraction, 1 Over 10, EndFraction'); + + // Testing plural numeric fractions from 31/2 to 31/10 + mq.latex('\\frac{31}{2}'); + assertMathspeak('31 halves'); + mq.latex('\\frac{31}{3}'); + assertMathspeak('31 thirds'); + mq.latex('\\frac{31}{4}'); + assertMathspeak('31 quarters'); + mq.latex('\\frac{31}{5}'); + assertMathspeak('31 fifths'); + mq.latex('\\frac{31}{6}'); + assertMathspeak('31 sixths'); + mq.latex('\\frac{31}{7}'); + assertMathspeak('31 sevenths'); + mq.latex('\\frac{31}{8}'); + assertMathspeak('31 eighths'); + mq.latex('\\frac{31}{9}'); + assertMathspeak('31 ninths'); + mq.latex('\\frac{31}{10}'); + assertMathspeak('StartFraction, 31 Over 10, EndFraction'); + + // Traditional fractions should be spoken if either numerator or denominator are not numeric + mq.latex('\\frac{x}{2}'); + assertMathspeak('StartFraction, "x" Over 2, EndFraction'); + mq.latex('\\frac{2}{x}'); + assertMathspeak('StartFraction, 2 Over "x", EndFraction'); + + // Traditional fractions should be spoken if either numerator or denominator are not whole numbers + mq.latex('\\frac{1.2}{2}'); + assertMathspeak('StartFraction, 1.2 Over 2, EndFraction'); + mq.latex('\\frac{4}{2.3}'); + assertMathspeak('StartFraction, 4 Over 2.3, EndFraction'); + + // A number followed by a shortened fraction should include the word "and", and other combinations should not. + mq.latex('3\\frac{3}{8}'); + assertMathspeak('3 and 3 eighths'); + mq.latex('3\\ \\frac{3}{8}'); + assertMathspeak('3 and 3 eighths'); + mq.latex('3\\frac{3}{x}'); + assertMathspeak('3 StartFraction, 3 Over "x", EndFraction'); + mq.latex('x\\frac{3}{8}'); + assertMathspeak('"x" 3 eighths'); + }); + + test('exponents', function() { + // Test simple superscripts and suffix rules + mq.latex('x^{0}'); + assertMathspeak('"x" to the 0 power'); + mq.latex('x^{1}'); + assertMathspeak('"x" to the 1st power'); + mq.latex('x^{2}'); + assertMathspeak('"x" squared'); + mq.latex('x^{3}'); + assertMathspeak('"x" cubed'); + mq.latex('x^{4}'); + assertMathspeak('"x" to the 4th power'); + mq.latex('x^{5}'); + assertMathspeak('"x" to the 5th power'); + mq.latex('x^{6}'); + assertMathspeak('"x" to the 6th power'); + mq.latex('x^{7}'); + assertMathspeak('"x" to the 7th power'); + mq.latex('x^{8}'); + assertMathspeak('"x" to the 8th power'); + mq.latex('x^{9}'); + assertMathspeak('"x" to the 9th power'); + mq.latex('x^{10}'); + assertMathspeak('"x" to the 10th power'); + mq.latex('x^{11}'); + assertMathspeak('"x" to the 11th power'); + mq.latex('x^{12}'); + assertMathspeak('"x" to the 12th power'); + mq.latex('x^{13}'); + assertMathspeak('"x" to the 13th power'); + mq.latex('x^{14}'); + assertMathspeak('"x" to the 14th power'); + mq.latex('x^{21}'); + assertMathspeak('"x" to the 21st power'); + mq.latex('x^{22}'); + assertMathspeak('"x" to the 22nd power'); + mq.latex('x^{23}'); + assertMathspeak('"x" to the 23rd power'); + mq.latex('x^{999}'); + assertMathspeak('"x" to the 999th power'); + // Values greater than 1000 have no suffix + mq.latex('x^{1000}'); + assertMathspeak('"x" to the 1000 power'); + mq.latex('x^{10000000000}'); + assertMathspeak('"x" to the 10000000000 power'); + + // Shorten negative exponents only if their parent is a digit + mq.latex('10^{-5}'); + assertMathspeak('10 to the negative 5th power'); + mq.latex('x^{-5}'); + assertMathspeak('"x" Superscript, negative 5, Baseline'); + + // Superscripts that are not strictly integers should continue to be spoken in longer form + mq.latex('x^{5.3}'); + assertMathspeak('"x" Superscript, 5.3, Baseline'); + mq.latex('x^{y}'); + assertMathspeak('"x" Superscript, "y", Baseline'); + mq.latex('x^{y^{2}}'); + assertMathspeak('"x" Superscript, "y" squared, Baseline'); + }); + + test('plus and minus differentiation', function() { + // Distinguish between positive vs plus and negative vs. minus + mq.latex('-25-25'); + assertMathspeak('negative 25 minus 25'); + mq.latex('+25+25'); + assertMathspeak('positive 25 plus 25'); + }); + }); + suite('auto-expanding parens', function() { suite('simple', function() { test('empty parens ()', function() { From 6242822a6d48eb94b3c565d141822da285aef3c1 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Thu, 22 Jul 2021 18:54:35 -0400 Subject: [PATCH 353/393] Fix shortening of fractions with negative numerators --- src/commands/math/commands.js | 41 ++++++++++++++++++----------------- test/unit/typing.test.js | 6 +++++ 2 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index eb7d13205..707897bfe 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -536,50 +536,51 @@ LatexCmds.fraction = P(MathCommand, function(_, super_) { return cursor.parent.mathspeak(); } - var numSpeech = this.ends[L].mathspeak(); - var denSpeech = this.ends[R].mathspeak(); + var numText = this.ends[L].text(); + var denText = this.ends[R].text(); // Shorten mathspeak value for whole number fractions whose denominator is less than 10. if ( (!opts || !opts.ignoreShorthand) && - intRgx.test(numSpeech) && intRgx.test(denSpeech) + intRgx.test(numText) && intRgx.test(denText) ) { + var isSingular = Math.abs(numText) === 1; var newDenSpeech = ''; - if (denSpeech === '2') { - newDenSpeech = numSpeech === '1' + if (denText === '2') { + newDenSpeech = isSingular ? 'half' : 'halves'; - } else if (denSpeech === '3') { - newDenSpeech = numSpeech === '1' + } else if (denText === '3') { + newDenSpeech = isSingular ? 'third' : 'thirds'; - } else if (denSpeech === '4') { - newDenSpeech = numSpeech === '1' + } else if (denText === '4') { + newDenSpeech = isSingular ? 'quarter' : 'quarters'; - } else if (denSpeech === '5') { - newDenSpeech = numSpeech === '1' + } else if (denText === '5') { + newDenSpeech = isSingular ? 'fifth' : 'fifths'; - } else if (denSpeech === '6') { - newDenSpeech = numSpeech === '1' + } else if (denText === '6') { + newDenSpeech = isSingular ? 'sixth' : 'sixths'; - } else if (denSpeech === '7') { - newDenSpeech = numSpeech === '1' + } else if (denText === '7') { + newDenSpeech = isSingular ? 'seventh' : 'sevenths'; - } else if (denSpeech === '8') { - newDenSpeech = numSpeech === '1' + } else if (denText === '8') { + newDenSpeech = isSingular ? 'eighth' : 'eighths'; - } else if (denSpeech === '9') { - newDenSpeech = numSpeech === '1' + } else if (denText === '9') { + newDenSpeech = isSingular ? 'ninth' : 'ninths'; } if (newDenSpeech !== '') { - return numSpeech + ' ' + newDenSpeech; + return this.ends[L].mathspeak() + ' ' + newDenSpeech; } } diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index b8d39d059..f6d7eef59 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -117,6 +117,12 @@ suite('typing with auto-replaces', function() { mq.latex('\\frac{31}{10}'); assertMathspeak('StartFraction, 31 Over 10, EndFraction'); + // Fractions with negative numerators should be shortened + mq.latex('\\frac{-1}{2}'); + assertMathspeak('negative 1 half'); + mq.latex('\\frac{-3}{2}'); + assertMathspeak('negative 3 halves'); + // Traditional fractions should be spoken if either numerator or denominator are not numeric mq.latex('\\frac{x}{2}'); assertMathspeak('StartFraction, "x" Over 2, EndFraction'); From 59151ca097166de92f159ac9250a4f317b05a413 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Thu, 22 Jul 2021 18:59:15 -0400 Subject: [PATCH 354/393] Remove negative exponent with non-numeric base restriction --- src/commands/math/commands.js | 41 +++++++++++++---------------------- test/unit/typing.test.js | 4 ++-- 2 files changed, 17 insertions(+), 28 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 707897bfe..006c0a63f 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -371,34 +371,23 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { } // More complex cases. - var isNegative = /^(-)/.test(innerText); - var isParentDigit = - this.parent && - this.parent.ends[L] && - this.parent.ends[L] instanceof Digit; - // If the superscript is negative and the parent is not a numeric literal, - // play it safe and don't shorten the mathspeak. - // For example, we don't know if a negative superscript is meant to signify exactly that - // or if it is being written to indicate an inverse function. - if (!isNegative || isParentDigit) { - var suffix = ''; - // Limit suffix addition to exponents < 1000. - if (Math.abs(innerText) < 1000) { - if (/(11|12|13|4|5|6|7|8|9|0)$/.test(innerText)) { - suffix = 'th'; - } else if (/1$/.test(innerText)) { - suffix = 'st'; - } else if (/2$/.test(innerText)) { - suffix = 'nd'; - } else if (/3$/.test(innerText)) { - suffix = 'rd'; - } + var suffix = ''; + // Limit suffix addition to exponents < 1000. + if (Math.abs(innerText) < 1000) { + if (/(11|12|13|4|5|6|7|8|9|0)$/.test(innerText)) { + suffix = 'th'; + } else if (/1$/.test(innerText)) { + suffix = 'st'; + } else if (/2$/.test(innerText)) { + suffix = 'nd'; + } else if (/3$/.test(innerText)) { + suffix = 'rd'; } - var innerMathspeak = typeof(child) === 'object' - ? child.mathspeak() - : innerText; - return 'to the ' + innerMathspeak + suffix + ' power'; } + var innerMathspeak = typeof(child) === 'object' + ? child.mathspeak() + : innerText; + return 'to the ' + innerMathspeak + suffix + ' power'; } } return super_.mathspeak.call(this); diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index f6d7eef59..46de453e1 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -192,11 +192,11 @@ suite('typing with auto-replaces', function() { mq.latex('x^{10000000000}'); assertMathspeak('"x" to the 10000000000 power'); - // Shorten negative exponents only if their parent is a digit + // Ensure negative exponents are shortened mq.latex('10^{-5}'); assertMathspeak('10 to the negative 5th power'); mq.latex('x^{-5}'); - assertMathspeak('"x" Superscript, negative 5, Baseline'); + assertMathspeak('"x" to the negative 5th power'); // Superscripts that are not strictly integers should continue to be spoken in longer form mq.latex('x^{5.3}'); From f8cde7d88005c8ebcd7ffe55ca4762edcbf9c0a3 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Mon, 26 Jul 2021 11:14:42 -0400 Subject: [PATCH 355/393] Respond to code review --- src/commands/math.js | 2 +- src/commands/math/basicSymbols.js | 47 ++++++++++++++++--------------- src/commands/math/commands.js | 4 +-- test/unit/publicapi.test.js | 2 +- 4 files changed, 29 insertions(+), 26 deletions(-) diff --git a/src/commands/math.js b/src/commands/math.js index 2ce335db3..65798807f 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -396,7 +396,7 @@ var MathBlock = P(MathElement, function(_, super_) { var cmdText = cmd.ctrlSeq; var isCmdNumeric = /^[\d]+$/.test(cmdText); - // Handle the case of a digit followed by a simplified fraction such as 1\frac{1}{2}. + // Handle the case of an integer followed by a simplified fraction such as 1\frac{1}{2}. // Such combinations should be spoken aloud as "1 and 1 half." if ( wasPrevNumeric && diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index 76f6e62fe..90ea032a8 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -788,31 +788,34 @@ LatexCmds['¾'] = bind(LatexFragment, '\\frac34'); LatexCmds['√'] = bind(LatexFragment, '\\sqrt{}'); var PlusMinus = P(BinaryOperator, function(_) { - _.init = VanillaSymbol.prototype.init; - _.contactWeld = _.siblingCreated = _.siblingDeleted = function(opts, dir) { - function determineOpClassType(node) { - if (node[L]) { - // If the left sibling is a binary operator or a separator (comma, semicolon, colon, 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)) { - return ''; - } - } else if (node.parent && node.parent.parent && node.parent.parent.isStyleBlock()) { - //if we are in a style block at the leftmost edge, determine unary/binary based on - //the style block - //this allows style blocks to be transparent for unary/binary purposes - return determineOpClassType(node.parent.parent); - } else { - return ''; + _.init = VanillaSymbol.prototype.init; + _.isBinaryOperator = function(node) { + if (node[L]) { + // 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)) { + return false; } + } else if (node.parent && node.parent.parent && node.parent.parent.isStyleBlock()) { + //if we are in a style block at the leftmost edge, determine unary/binary based on + //the style block + //this allows style blocks to be transparent for unary/binary purposes + return this.isBinaryOperator(node.parent.parent); + } else { + return false; + } - return 'mq-binary-operator'; - }; + return true; + }; + _.contactWeld = _.siblingCreated = _.siblingDeleted = function(opts, dir) { if (dir === R) return; // ignore if sibling only changed on the right - this.jQ[0].className = determineOpClassType(this); + this.jQ[0].className = this.isBinaryOperator(this) + ? 'mq-binary-operator' + : ''; + return this; }; }); @@ -822,7 +825,7 @@ LatexCmds['+'] = P(PlusMinus, function(_, super_) { super_.init.call(this, '+', '+'); }; _.mathspeak = function() { - return this.jQ[0].className === 'mq-binary-operator' ? 'plus' : 'positive'; + return this.isBinaryOperator(this) ? 'plus' : 'positive'; }; }); @@ -832,7 +835,7 @@ LatexCmds['−'] = LatexCmds['—'] = LatexCmds['–'] = LatexCmds['-'] = P(Plus super_.init.call(this, '-', '−'); }; _.mathspeak = function() { - return this.jQ[0].className === 'mq-binary-operator' ? 'minus' : 'negative'; + return this.isBinaryOperator(this) ? 'minus' : 'negative'; }; }); diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 006c0a63f..6a14197e3 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -167,7 +167,7 @@ var Class = LatexCmds['class'] = P(MathCommand, function(_, super_) { // 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. -var intRgx = new RegExp(/^[\+\-]?[\d]+$/); +var intRgx = /^[\+\-]?[\d]+$/; var SupSub = P(MathCommand, function(_, super_) { _.ctrlSeq = '_{...}^{...}'; @@ -348,7 +348,7 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { _.textTemplate = [ '^' ]; _.mathspeak = function(opts) { // Simplify basic exponent speech for common whole numbers. - var child = this.ends[L] || this.ends[R]; + var child = this.ends[L]; if (child !== undefined) { // Calculate this item's inner text to determine whether to shorten the returned speech. // Do not calculate its inner mathspeak now until we know that the speech is to be truncated. diff --git a/test/unit/publicapi.test.js b/test/unit/publicapi.test.js index 06ec80d31..c99f38164 100644 --- a/test/unit/publicapi.test.js +++ b/test/unit/publicapi.test.js @@ -184,7 +184,7 @@ suite('Public API', function() { assertMathSpeakEqual(mq.mathspeak(), 'StartFraction "d" Over "d" "x" EndFraction StartRoot "x" EndRoot'); mq.latex('1+2-3\\cdot\\frac{5}{6^7}=\\left(8+9\\right)'); - assertMathSpeakEqual(mq.mathspeak(), '1 plus 2 minus 3 times StartFraction 5 Over 6 to the 7 th power EndFraction equals left parenthesis 8 plus 9 right parenthesis'); + assertMathSpeakEqual(mq.mathspeak(), '1 plus 2 minus 3 times StartFraction 5 Over 6 to the 7th power EndFraction equals left parenthesis 8 plus 9 right parenthesis'); // Example 13 from http://www.gh-mathspeak.com/examples/quick-tutorial/index.php?verbosity=v&explicitness=2&interp=0 mq.latex('d=\\sqrt{ \\left( x_2 - x_1 \\right)^2 - \\left( y_2 - y_1 \\right)^2 }'); From d931bde89f555abf54260fd5ca33431a656b9a29 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Mon, 26 Jul 2021 11:34:48 -0400 Subject: [PATCH 356/393] Factor isBinaryOperator into stand-alone function --- src/commands/math/basicSymbols.js | 46 ++++++++++++++++--------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index 90ea032a8..b44ecc556 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -787,32 +787,34 @@ LatexCmds['¾'] = bind(LatexFragment, '\\frac34'); // around handling valid latex as latex rather than treating it as keystrokes. LatexCmds['√'] = bind(LatexFragment, '\\sqrt{}'); -var PlusMinus = P(BinaryOperator, function(_) { - - _.init = VanillaSymbol.prototype.init; - _.isBinaryOperator = function(node) { - if (node[L]) { - // 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)) { - return false; - } - } else if (node.parent && node.parent.parent && node.parent.parent.isStyleBlock()) { - //if we are in a style block at the leftmost edge, determine unary/binary based on - //the style block - //this allows style blocks to be transparent for unary/binary purposes - return this.isBinaryOperator(node.parent.parent); - } else { +// 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]) { + // 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)) { return false; } + } else if (node.parent && node.parent.parent && node.parent.parent.isStyleBlock()) { + //if we are in a style block at the leftmost edge, determine unary/binary based on + //the style block + //this allows style blocks to be transparent for unary/binary purposes + return isBinaryOperator(node.parent.parent); + } else { + return false; + } - return true; - }; + return true; +} +var PlusMinus = P(BinaryOperator, function(_) { + + _.init = VanillaSymbol.prototype.init; _.contactWeld = _.siblingCreated = _.siblingDeleted = function(opts, dir) { if (dir === R) return; // ignore if sibling only changed on the right - this.jQ[0].className = this.isBinaryOperator(this) + this.jQ[0].className = isBinaryOperator(this) ? 'mq-binary-operator' : ''; @@ -825,7 +827,7 @@ LatexCmds['+'] = P(PlusMinus, function(_, super_) { super_.init.call(this, '+', '+'); }; _.mathspeak = function() { - return this.isBinaryOperator(this) ? 'plus' : 'positive'; + return isBinaryOperator(this) ? 'plus' : 'positive'; }; }); @@ -835,7 +837,7 @@ LatexCmds['−'] = LatexCmds['—'] = LatexCmds['–'] = LatexCmds['-'] = P(Plus super_.init.call(this, '-', '−'); }; _.mathspeak = function() { - return this.isBinaryOperator(this) ? 'minus' : 'negative'; + return isBinaryOperator(this) ? 'minus' : 'negative'; }; }); From 44de07f27736f949534290ff9fb220392a46ff54 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Mon, 26 Jul 2021 12:29:17 -0400 Subject: [PATCH 357/393] User superscript's upInto node in mathspeak method --- src/commands/math/commands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 6a14197e3..c9e53ae74 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -348,7 +348,7 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { _.textTemplate = [ '^' ]; _.mathspeak = function(opts) { // Simplify basic exponent speech for common whole numbers. - var child = this.ends[L]; + var child = this.upInto; if (child !== undefined) { // Calculate this item's inner text to determine whether to shorten the returned speech. // Do not calculate its inner mathspeak now until we know that the speech is to be truncated. From ce15049fb23b3acb1d06aea157dd524a5e5ac0b1 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Mon, 26 Jul 2021 14:53:57 -0400 Subject: [PATCH 358/393] Use concatenation of block's ctrlSeqs rather than potentially expensive text() method for shortened mathspeak computation --- src/commands/math/commands.js | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index c9e53ae74..33b4ea16e 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -169,6 +169,28 @@ var Class = LatexCmds['class'] = P(MathCommand, function(_, super_) { // for shortening the verbalized (mathspeak) forms of some fractions and superscripts. 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; + var children = block.children(); + if (!children || !children.ends[L]) return block; + var chars = ''; + for (var sibling = children.ends[L]; sibling[R] !== undefined; sibling = sibling[R]) { + if (sibling.ctrlSeq !== undefined) { + chars += sibling.ctrlSeq; + } else { + break; + } + } + return chars; +} + var SupSub = P(MathCommand, function(_, super_) { _.ctrlSeq = '_{...}^{...}'; _.createLeftOf = function(cursor) { @@ -353,9 +375,7 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { // Calculate this item's inner text to determine whether to shorten the returned speech. // Do not calculate its inner mathspeak now until we know that the speech is to be truncated. // Since the mathspeak computation is recursive, we want to call it only once in this function to avoid performance bottlenecks. - var innerText = typeof(child) === 'object' - ? child.text() - : ''+child; + var innerText = getCtrlSeqsFromBlock(child); // If the superscript is a whole number, shorten the speech that is returned. if ( (!opts || !opts.ignoreShorthand) && @@ -525,8 +545,8 @@ LatexCmds.fraction = P(MathCommand, function(_, super_) { return cursor.parent.mathspeak(); } - var numText = this.ends[L].text(); - var denText = this.ends[R].text(); + var numText = getCtrlSeqsFromBlock(this.ends[L]); + var denText = getCtrlSeqsFromBlock(this.ends[R]); // Shorten mathspeak value for whole number fractions whose denominator is less than 10. if ( From ea3af83c198895bb1f20144a20ce2e25d14be8c6 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Mon, 26 Jul 2021 17:11:35 -0400 Subject: [PATCH 359/393] Remove ctrlSeq existence check --- src/commands/math/commands.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 33b4ea16e..01afc5d4d 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -182,11 +182,7 @@ function getCtrlSeqsFromBlock(block) { if (!children || !children.ends[L]) return block; var chars = ''; for (var sibling = children.ends[L]; sibling[R] !== undefined; sibling = sibling[R]) { - if (sibling.ctrlSeq !== undefined) { - chars += sibling.ctrlSeq; - } else { - break; - } + chars += sibling.ctrlSeq; } return chars; } From 6b5bb8d951639930cac101ad3c4d672053ddbac9 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 27 Jul 2021 14:55:59 -0400 Subject: [PATCH 360/393] Re-add ctrlSeq check Unlike before, however, don't abort if the current element doesn't have one but there are more siblings to traverse. --- src/commands/math/commands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 01afc5d4d..781887152 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -182,7 +182,7 @@ function getCtrlSeqsFromBlock(block) { if (!children || !children.ends[L]) return block; var chars = ''; for (var sibling = children.ends[L]; sibling[R] !== undefined; sibling = sibling[R]) { - chars += sibling.ctrlSeq; + if (sibling.ctrlSeq !== undefined) chars += sibling.ctrlSeq; } return chars; } From 46498ff6e77459df6b0f462feacf26994f84cb04 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 27 Jul 2021 15:00:27 -0400 Subject: [PATCH 361/393] Stop using Math.abs on strings --- src/commands/math/commands.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 781887152..80479a184 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -389,7 +389,8 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { // More complex cases. var suffix = ''; // Limit suffix addition to exponents < 1000. - if (Math.abs(innerText) < 1000) { + var innerNumber = parseInt(innerText, 10); + if (innerNumber !== NaN && Math.abs(innerNumber) < 1000) { if (/(11|12|13|4|5|6|7|8|9|0)$/.test(innerText)) { suffix = 'th'; } else if (/1$/.test(innerText)) { @@ -549,7 +550,7 @@ LatexCmds.fraction = P(MathCommand, function(_, super_) { (!opts || !opts.ignoreShorthand) && intRgx.test(numText) && intRgx.test(denText) ) { - var isSingular = Math.abs(numText) === 1; + var isSingular = numText === 1 || numText === '-1'; var newDenSpeech = ''; if (denText === '2') { newDenSpeech = isSingular From 3fcd180ceece580902c6d808db70296eb95c70df Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 27 Jul 2021 15:58:41 -0400 Subject: [PATCH 362/393] Move mixed fraction speech determination to fraction mathspeak method Instead of just checking whether the previous mathspeak character was a number, we now scan leftward from the fraction element itself and insert the and conjunction if all items to the left of the current block are digits, the plus or minus sign, or whitespace. In particular, this stops Mathquill from reporting 'and' for decimals followed by fractions. --- src/commands/math.js | 23 +++-------------------- src/commands/math/commands.js | 21 +++++++++++++++++++-- 2 files changed, 22 insertions(+), 22 deletions(-) diff --git a/src/commands/math.js b/src/commands/math.js index 65798807f..ff4f395e2 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -379,11 +379,11 @@ var MathBlock = P(MathElement, function(_, super_) { var tempOp = ''; var autoOps = {}; if (this.controller) autoOps = this.controller.options.autoOperatorNames; - var wasPrevNumeric = false; return this.foldChildren([], function(speechArray, cmd) { if (cmd.isPartOfOperator) { tempOp += cmd.mathspeak(); - } else { + } + else { if(tempOp!=='') { if(autoOps !== {} && autoOps._maxLength > 0) { var x = autoOps[tempOp.toLowerCase()]; @@ -394,27 +394,10 @@ var MathBlock = P(MathElement, function(_, super_) { } var mathspeakText = cmd.mathspeak(); var cmdText = cmd.ctrlSeq; - var isCmdNumeric = /^[\d]+$/.test(cmdText); - - // Handle the case of an integer followed by a simplified fraction such as 1\frac{1}{2}. - // Such combinations should be spoken aloud as "1 and 1 half." - if ( - wasPrevNumeric && - cmdText.indexOf('frac') !== -1 && - mathspeakText.indexOf('Fraction') === -1 - ) { - speechArray.push(' and '); - } - - if (!isCmdNumeric && cmdText !== '.') { + if (isNaN(cmdText) && cmdText !== '.') { mathspeakText = ' ' + mathspeakText + ' '; } speechArray.push(mathspeakText); - // Update wasPrevNumeric ignoring whitespace - // e.g. We want to make 1\frac{1}{2} equivalent speech-wise to 1 \frac{1}{2}. - if (cmdText !== '\\ ') { - wasPrevNumeric = isCmdNumeric; - } } return speechArray; }) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 80479a184..694c061c6 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -550,7 +550,7 @@ LatexCmds.fraction = P(MathCommand, function(_, super_) { (!opts || !opts.ignoreShorthand) && intRgx.test(numText) && intRgx.test(denText) ) { - var isSingular = numText === 1 || numText === '-1'; + var isSingular = numText === '1' || numText === '-1'; var newDenSpeech = ''; if (denText === '2') { newDenSpeech = isSingular @@ -586,7 +586,24 @@ LatexCmds.fraction = P(MathCommand, function(_, super_) { : 'ninths'; } if (newDenSpeech !== '') { - return this.ends[L].mathspeak() + ' ' + newDenSpeech; + var output = ''; + // Handle the case of an integer followed by a simplified fraction such as 1\frac{1}{2}. + // 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]) { + if (sibling.ctrlSeq === '\\ ' || intRgx.test(sibling.ctrlSeq)) { + precededByInteger = true; + } else { + precededByInteger = false; + break; + } + } + if (precededByInteger) { + output += 'and '; + } + output += this.ends[L].mathspeak() + ' ' + newDenSpeech; + return output; } } From 0f1fabeb8a2274779ebcad25661429dd8a7141bb Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 27 Jul 2021 16:08:07 -0400 Subject: [PATCH 363/393] Add fraction tests --- test/unit/typing.test.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index 46de453e1..498af6ffe 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -122,6 +122,12 @@ suite('typing with auto-replaces', function() { assertMathspeak('negative 1 half'); mq.latex('\\frac{-3}{2}'); assertMathspeak('negative 3 halves'); + mq.latex('-\\frac{3}{4}'); + assertMathspeak('negative 3 quarters'); + + // Fractions with negative denominators should not be shortened + mq.latex('\\frac{1}{-2}'); + assertMathspeak('StartFraction, 1 Over negative 2, EndFraction'); // Traditional fractions should be spoken if either numerator or denominator are not numeric mq.latex('\\frac{x}{2}'); @@ -135,11 +141,13 @@ suite('typing with auto-replaces', function() { mq.latex('\\frac{4}{2.3}'); assertMathspeak('StartFraction, 4 Over 2.3, EndFraction'); - // A number followed by a shortened fraction should include the word "and", and other combinations should not. + // A whole number followed by a shortened fraction should include the word "and", and other combinations should not. mq.latex('3\\frac{3}{8}'); assertMathspeak('3 and 3 eighths'); mq.latex('3\\ \\frac{3}{8}'); assertMathspeak('3 and 3 eighths'); + mq.latex('3.1\\frac{3}{8}'); + assertMathspeak('3.1 3 eighths'); mq.latex('3\\frac{3}{x}'); assertMathspeak('3 StartFraction, 3 Over "x", EndFraction'); mq.latex('x\\frac{3}{8}'); From 1ae3939f9c0b75d7455ebe2b0c3b25abc6ddc257 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 27 Jul 2021 16:14:22 -0400 Subject: [PATCH 364/393] Add test for speaking longer fraction when Tabbing out of it --- test/unit/aria.test.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/unit/aria.test.js b/test/unit/aria.test.js index b4ecd4da8..409eece43 100644 --- a/test/unit/aria.test.js +++ b/test/unit/aria.test.js @@ -38,6 +38,10 @@ suite('aria', function() { assertAriaEqual('over'); mathField.typedText('2'); assertAriaEqual('2'); + mathField.keystroke('Tab'); + assertAriaEqual('after StartFraction, 1 Over 2 , EndFraction'); + mathField.keystroke('Backspace'); + assertAriaEqual('end of denominator 2'); mathField.keystroke('Backspace'); assertAriaEqual('2'); mathField.keystroke('Backspace'); From 5298e5c7a5c8e4b4f9b01455d448e2ead62e529a Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 27 Jul 2021 22:56:22 -0400 Subject: [PATCH 365/393] Respond to code review --- src/commands/math/commands.js | 8 +++++--- test/unit/typing.test.js | 8 ++++++++ 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 694c061c6..f24c498aa 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -389,8 +389,7 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { // More complex cases. var suffix = ''; // Limit suffix addition to exponents < 1000. - var innerNumber = parseInt(innerText, 10); - if (innerNumber !== NaN && Math.abs(innerNumber) < 1000) { + if (/^[+-]?\d{1,3}$/.test(innerText)) { if (/(11|12|13|4|5|6|7|8|9|0)$/.test(innerText)) { suffix = 'th'; } else if (/1$/.test(innerText)) { @@ -592,7 +591,10 @@ LatexCmds.fraction = P(MathCommand, function(_, super_) { // 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]) { - if (sibling.ctrlSeq === '\\ ' || intRgx.test(sibling.ctrlSeq)) { + // Ignore whitespace + if (sibling.ctrlSeq === '\\ ') { + continue; + } else if (intRgx.test(sibling.ctrlSeq)) { precededByInteger = true; } else { precededByInteger = false; diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index 498af6ffe..42aa7ec8f 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -146,8 +146,16 @@ suite('typing with auto-replaces', function() { assertMathspeak('3 and 3 eighths'); mq.latex('3\\ \\frac{3}{8}'); assertMathspeak('3 and 3 eighths'); + mq.latex('3\\ \\ \\ \\ \\ \\frac{3}{8}'); + assertMathspeak('3 and 3 eighths'); mq.latex('3.1\\frac{3}{8}'); assertMathspeak('3.1 3 eighths'); + mq.latex('3.1\\ \\frac{3}{8}'); + assertMathspeak('3.1 3 eighths'); + mq.latex('3.1\\ \\ \\ \\ \\frac{3}{8}'); + assertMathspeak('3.1 3 eighths'); + mq.latex('\\ \\frac{1}{2}'); + assertMathspeak('1 half'); mq.latex('3\\frac{3}{x}'); assertMathspeak('3 StartFraction, 3 Over "x", EndFraction'); mq.latex('x\\frac{3}{8}'); From 4bc7ea17c3e65e1d85a901b4198c732ba52865f2 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Wed, 28 Jul 2021 10:14:34 -0400 Subject: [PATCH 366/393] Add explanatory comment to new ARIA test --- test/unit/aria.test.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/unit/aria.test.js b/test/unit/aria.test.js index 409eece43..80385e50d 100644 --- a/test/unit/aria.test.js +++ b/test/unit/aria.test.js @@ -38,8 +38,14 @@ suite('aria', function() { assertAriaEqual('over'); mathField.typedText('2'); assertAriaEqual('2'); + + // We have logic to shorten the speak we return for common numeric fractions and superscripts. + // While editing, however, the slightly longer form (but unambiguous) form of the item should be spoken. + // In this case, we would shorten the fraction 1/2 to "1 half" when reading, + // but navigating around the equation should result in "StartFraction, 1 Over 2, EndFraction." mathField.keystroke('Tab'); assertAriaEqual('after StartFraction, 1 Over 2 , EndFraction'); + mathField.keystroke('Backspace'); assertAriaEqual('end of denominator 2'); mathField.keystroke('Backspace'); From 999deb5757b354bddac3b33aa8f24547a48630e8 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 17 Aug 2021 15:37:54 -0400 Subject: [PATCH 367/393] Improve mathspeak for text blocks In particular, apply the same ignoreShorthand logic to start text and end text announcements that we use to differentiate block editing vs. generic reading. --- src/commands/text.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/commands/text.js b/src/commands/text.js index 887322a51..38ba90031 100644 --- a/src/commands/text.js +++ b/src/commands/text.js @@ -75,7 +75,13 @@ var TextBlock = P(Node, function(_, super_) { ); }; _.mathspeakTemplate = ['Start'+_.ariaLabel, 'End'+_.ariaLabel]; - _.mathspeak = function() { return this.mathspeakTemplate[0]+', '+this.text() +', '+this.mathspeakTemplate[1] }; + _.mathspeak = function(opts) { + if (opts && opts.ignoreShorthand) { + return this.mathspeakTemplate[0]+', '+this.text() +', '+this.mathspeakTemplate[1] + } else { + return this.text(); + } + }; // editability methods: called by the cursor for editing, cursor movements, // and selection of the MathQuill tree, these all take in a direction and From 0da38da93d91600bad108eba77f7b06dd992e5c3 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 17 Aug 2021 18:22:01 -0400 Subject: [PATCH 368/393] Improve mathspeak for font style, textcolor, and class blocks For general mathspeak computation, ignore the start and end block delimiters. Speak them when navigating. Also stops separating individual letters inside style blocks so they are read aloud as normal text. --- src/commands/math.js | 9 ++++++--- src/commands/math/basicSymbols.js | 5 +++-- src/commands/math/commands.js | 31 +++++++++++++++++++++++++++++++ 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/src/commands/math.js b/src/commands/math.js index ff4f395e2..cb731730f 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -382,8 +382,7 @@ var MathBlock = P(MathElement, function(_, super_) { return this.foldChildren([], function(speechArray, cmd) { if (cmd.isPartOfOperator) { tempOp += cmd.mathspeak(); - } - else { + } else { if(tempOp!=='') { if(autoOps !== {} && autoOps._maxLength > 0) { var x = autoOps[tempOp.toLowerCase()]; @@ -394,7 +393,11 @@ var MathBlock = P(MathElement, function(_, super_) { } var mathspeakText = cmd.mathspeak(); var cmdText = cmd.ctrlSeq; - if (isNaN(cmdText) && cmdText !== '.') { + if ( + isNaN(cmdText) && + cmdText !== '.' && + !(cmd.parent && cmd.parent.parent && cmd.parent.parent.isStyleBlock()) + ) { mathspeakText = ' ' + mathspeakText + ' '; } speechArray.push(mathspeakText); diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index b44ecc556..ca6a908cc 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -212,13 +212,15 @@ var Variable = P(Symbol, function(_, super_) { var text = this.ctrlSeq; if (this.isPartOfOperator || text.length !== 1) { return super_.mathspeak.call(this); + } else if (this.parent && this.parent.parent && this.parent.parent.isStyleBlock()) { + return text; } else { // Apple voices in VoiceOver (such as Alex, Bruce, and Victoria) do // some strange pronunciation given certain expressions, // e.g. "y-2" is spoken as "ee minus 2" (as if the y is short). // Not an ideal solution, but surrounding non-numeric text blocks with quotation marks works. // This bug has been acknowledged by Apple. - return '"' + text + '"'; + return '"'+text+'"'; } }; }); @@ -263,7 +265,6 @@ optionProcessors.autoParenthesizedFunctions = function (cmds) { } var Letter = P(Variable, function(_, super_) { - _.init = function(ch) { return super_.init.call(this, this.letter = ch); }; _.checkAutoCmds = function (cursor) { //handle autoCommands diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index f24c498aa..581f2a664 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -86,6 +86,17 @@ var Style = P(MathCommand, function(_, super_) { _.ariaLabel = ariaLabel || ctrlSeq.replace(/^\\/, ''); _.mathspeakTemplate = ['Start' + _.ariaLabel + ',', 'End' + _.ariaLabel]; }; + _.mathspeak = function(opts) { + if (opts && opts.ignoreShorthand) { + return super_.mathspeak.call(this); + } + return this.foldChildren('', function(speech, block) { + return speech + ' ' + block.mathspeak(); + }).trim(); + }; + _.isStyleBlock = function() { + return true; + }; }); //fonts @@ -114,6 +125,8 @@ var TextColor = LatexCmds.textcolor = P(MathCommand, function(_, super_) { this.color = color; this.htmlTemplate = '&0'; + _.ariaLabel = color.replace(/^\\/, ''); + _.mathspeakTemplate = ['Start ' + _.ariaLabel + ',', 'End ' + _.ariaLabel]; }; _.latex = function() { return '\\textcolor{' + this.color + '}{' + this.blocks[0].latex() + '}'; @@ -134,6 +147,14 @@ var TextColor = LatexCmds.textcolor = P(MathCommand, function(_, super_) { }) ; }; + _.mathspeak = function(opts) { + if (opts && opts.ignoreShorthand) { + return super_.mathspeak.call(this); + } + return this.foldChildren('', function(speech, block) { + return speech + ' ' + block.mathspeak(); + }).trim(); + }; _.isStyleBlock = function() { return true; }; @@ -153,6 +174,8 @@ var Class = LatexCmds['class'] = P(MathCommand, function(_, super_) { .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); }) ; @@ -160,6 +183,14 @@ var Class = LatexCmds['class'] = P(MathCommand, function(_, super_) { _.latex = function() { return '\\class{' + this.cls + '}{' + this.blocks[0].latex() + '}'; }; + _.mathspeak = function(opts) { + if (opts && opts.ignoreShorthand) { + return super_.mathspeak.call(this); + } + return this.foldChildren('', function(speech, block) { + return speech + ' ' + block.mathspeak(); + }).trim(); + }; _.isStyleBlock = function() { return true; }; From 103d04ed660f9e7a84f6fd43ef93796fd5dd2e0c Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Wed, 18 Aug 2021 12:31:14 -0400 Subject: [PATCH 369/393] Continue to speak start and end delimiters for overline --- src/commands/math/commands.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 581f2a664..59e6ae072 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -81,13 +81,19 @@ var SVG_SYMBOLS = { }; var Style = P(MathCommand, function(_, super_) { - _.init = function(ctrlSeq, tagName, attrs, ariaLabel) { + _.init = function(ctrlSeq, tagName, attrs, ariaLabel, shouldSpeakDelimiters) { super_.init.call(this, ctrlSeq, '<'+tagName+' '+attrs+'>&0'); _.ariaLabel = ariaLabel || ctrlSeq.replace(/^\\/, ''); _.mathspeakTemplate = ['Start' + _.ariaLabel + ',', 'End' + _.ariaLabel]; + // In most cases, mathspeak should not announce the start and end of style blocks. + // There is one exception currently (overline). + _.shouldSpeakDelimiters = !!shouldSpeakDelimiters; }; _.mathspeak = function(opts) { - if (opts && opts.ignoreShorthand) { + if ( + this.shouldSpeakDelimiters || + (opts && opts.ignoreShorthand) + ) { return super_.mathspeak.call(this); } return this.foldChildren('', function(speech, block) { @@ -107,7 +113,7 @@ LatexCmds.mathsf = bind(Style, '\\mathsf', 'span', 'class="mq-sans-serif mq-font LatexCmds.mathtt = bind(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.overline = LatexCmds.bar = bind(Style, '\\overline', 'span', 'class="mq-non-leaf mq-overline"', 'Overline', true); 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'); From 7f88540a699918fbcb30233449c235486dc08323 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Wed, 18 Aug 2021 17:19:58 -0400 Subject: [PATCH 370/393] Pair back delimiter omission to only text and mathrm commands Also adds tests. --- src/commands/math.js | 2 +- src/commands/math/basicSymbols.js | 8 ++++--- src/commands/math/commands.js | 40 +++++++++++-------------------- src/commands/text.js | 7 ++++-- src/tree.js | 4 ++++ test/unit/typing.test.js | 22 +++++++++++++++++ 6 files changed, 51 insertions(+), 32 deletions(-) diff --git a/src/commands/math.js b/src/commands/math.js index cb731730f..50ce7781d 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -396,7 +396,7 @@ var MathBlock = P(MathElement, function(_, super_) { if ( isNaN(cmdText) && cmdText !== '.' && - !(cmd.parent && cmd.parent.parent && cmd.parent.parent.isStyleBlock()) + (!cmd.parent || !cmd.parent.parent || !cmd.parent.parent.isTextBlock()) ) { mathspeakText = ' ' + mathspeakText + ' '; } diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index ca6a908cc..9f1c3058b 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -210,10 +210,12 @@ var Variable = P(Symbol, function(_, super_) { }; _.mathspeak = function() { var text = this.ctrlSeq; - if (this.isPartOfOperator || text.length !== 1) { + if ( + this.isPartOfOperator || + text.length > 1 || + (this.parent && this.parent.parent && this.parent.parent.isTextBlock()) + ) { return super_.mathspeak.call(this); - } else if (this.parent && this.parent.parent && this.parent.parent.isStyleBlock()) { - return text; } else { // Apple voices in VoiceOver (such as Alex, Bruce, and Victoria) do // some strange pronunciation given certain expressions, diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 59e6ae072..f63e597af 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -81,17 +81,17 @@ var SVG_SYMBOLS = { }; var Style = P(MathCommand, function(_, super_) { - _.init = function(ctrlSeq, tagName, attrs, ariaLabel, shouldSpeakDelimiters) { + _.init = function(ctrlSeq, tagName, attrs, ariaLabel, shouldNotSpeakDelimiters) { super_.init.call(this, ctrlSeq, '<'+tagName+' '+attrs+'>&0'); _.ariaLabel = ariaLabel || ctrlSeq.replace(/^\\/, ''); _.mathspeakTemplate = ['Start' + _.ariaLabel + ',', 'End' + _.ariaLabel]; - // In most cases, mathspeak should not announce the start and end of style blocks. - // There is one exception currently (overline). - _.shouldSpeakDelimiters = !!shouldSpeakDelimiters; + // In most cases, mathspeak should announce the start and end of style blocks. + // There is one exception currently (mathrm). + _.shouldNotSpeakDelimiters = !!shouldNotSpeakDelimiters; }; _.mathspeak = function(opts) { if ( - this.shouldSpeakDelimiters || + !this.shouldNotSpeakDelimiters || (opts && opts.ignoreShorthand) ) { return super_.mathspeak.call(this); @@ -100,20 +100,24 @@ var Style = P(MathCommand, function(_, super_) { return speech + ' ' + block.mathspeak(); }).trim(); }; - _.isStyleBlock = function() { - return true; - }; }); //fonts -LatexCmds.mathrm = bind(Style, '\\mathrm', 'span', 'class="mq-roman mq-font"', 'Roman Font'); +LatexCmds.mathrm = P(Style, function(_, super_) { + _.init = function() { + super_.init.call(this, '\\mathrm', 'span', 'class="mq-roman mq-font"', 'Roman Font', true); + }; + _.isTextBlock = function() { + 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'); //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', true); +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'); @@ -153,14 +157,6 @@ var TextColor = LatexCmds.textcolor = P(MathCommand, function(_, super_) { }) ; }; - _.mathspeak = function(opts) { - if (opts && opts.ignoreShorthand) { - return super_.mathspeak.call(this); - } - return this.foldChildren('', function(speech, block) { - return speech + ' ' + block.mathspeak(); - }).trim(); - }; _.isStyleBlock = function() { return true; }; @@ -189,14 +185,6 @@ var Class = LatexCmds['class'] = P(MathCommand, function(_, super_) { _.latex = function() { return '\\class{' + this.cls + '}{' + this.blocks[0].latex() + '}'; }; - _.mathspeak = function(opts) { - if (opts && opts.ignoreShorthand) { - return super_.mathspeak.call(this); - } - return this.foldChildren('', function(speech, block) { - return speech + ' ' + block.mathspeak(); - }).trim(); - }; _.isStyleBlock = function() { return true; }; diff --git a/src/commands/text.js b/src/commands/text.js index 38ba90031..0e5f95f4f 100644 --- a/src/commands/text.js +++ b/src/commands/text.js @@ -77,11 +77,14 @@ var TextBlock = P(Node, function(_, super_) { _.mathspeakTemplate = ['Start'+_.ariaLabel, 'End'+_.ariaLabel]; _.mathspeak = function(opts) { if (opts && opts.ignoreShorthand) { - return this.mathspeakTemplate[0]+', '+this.text() +', '+this.mathspeakTemplate[1] + return this.mathspeakTemplate[0]+', '+this.textContents() +', '+this.mathspeakTemplate[1] } else { - return this.text(); + 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 diff --git a/src/tree.js b/src/tree.js index 7732c99c3..5eb463493 100644 --- a/src/tree.js +++ b/src/tree.js @@ -275,6 +275,10 @@ var Node = P(function(_) { return false; }; + _.isTextBlock = function() { + return false; + }; + _.children = function() { return Fragment(this.ends[L], this.ends[R]); }; diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index 42aa7ec8f..c7c081816 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -230,6 +230,28 @@ suite('typing with auto-replaces', function() { mq.latex('+25+25'); assertMathspeak('positive 25 plus 25'); }); + + test('styled text', function() { + // Test that text-related elements include sensible mathspeak. + // Letters in a non-wrapped block should be split apart (interpreted as variables): + mq.latex('this is a test'); + assertMathspeak('"t" "h" "i" "s" "i" "s" "a" "t" "e" "s" "t"'); + // Contents of a text block should be returned exactly as entered with no start and end delimiters spoken: + mq.latex('\\text{this is a test}'); + assertMathspeak('this is a test'); + // Specifically for mathrm, don't split characters and also don't speak delimiters. + // note content is still interpreted as LaTeX, so we use \ to separate words: + mq.latex('\\mathrm{this\\ is\\ a\\ test}'); + assertMathspeak('this is a test'); + // Any other font command should be spoken "normally"-- + // letters are split and delimiters are announced for remaining commands: + mq.latex('\\mathit{this\\ is\\ a\\ test}'); + assertMathspeak('StartItalic Font "t" "h" "i" "s" "i" "s" "a" "t" "e" "s" "t" EndItalic Font'); + mq.latex('\\textcolor{red}{this\\ is\\ a\\ test}'); + assertMathspeak('Start red "t" "h" "i" "s" "i" "s" "a" "t" "e" "s" "t" End red'); + mq.latex('\\class{abc}{this\\ is\\ a\\ test}'); + assertMathspeak('Start abc class "t" "h" "i" "s" "i" "s" "a" "t" "e" "s" "t" End abc class'); + }); }); suite('auto-expanding parens', function() { From e355e7c069522da14fded3c7874d7e138a2c67bc Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Thu, 19 Aug 2021 11:44:40 -0400 Subject: [PATCH 371/393] Be sure to pass mathspeak opts on to child blocks from math style command --- src/commands/math/commands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index f63e597af..30d8c3672 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -97,7 +97,7 @@ var Style = P(MathCommand, function(_, super_) { return super_.mathspeak.call(this); } return this.foldChildren('', function(speech, block) { - return speech + ' ' + block.mathspeak(); + return speech + ' ' + block.mathspeak(opts); }).trim(); }; }); From 2c283dc2df71481fafcb44af51105aeb4f340f80 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Thu, 19 Aug 2021 12:14:33 -0400 Subject: [PATCH 372/393] Use options object in style initialiation for clarity --- src/commands/math/commands.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 30d8c3672..8ac1345d4 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -81,13 +81,13 @@ var SVG_SYMBOLS = { }; var Style = P(MathCommand, function(_, super_) { - _.init = function(ctrlSeq, tagName, attrs, ariaLabel, shouldNotSpeakDelimiters) { + _.init = function(ctrlSeq, tagName, attrs, ariaLabel, opts) { super_.init.call(this, ctrlSeq, '<'+tagName+' '+attrs+'>&0'); _.ariaLabel = ariaLabel || ctrlSeq.replace(/^\\/, ''); _.mathspeakTemplate = ['Start' + _.ariaLabel + ',', 'End' + _.ariaLabel]; // In most cases, mathspeak should announce the start and end of style blocks. // There is one exception currently (mathrm). - _.shouldNotSpeakDelimiters = !!shouldNotSpeakDelimiters; + _.shouldNotSpeakDelimiters = opts && opts.shouldNotSpeakDelimiters; }; _.mathspeak = function(opts) { if ( @@ -105,7 +105,7 @@ var Style = P(MathCommand, function(_, super_) { //fonts LatexCmds.mathrm = P(Style, function(_, super_) { _.init = function() { - super_.init.call(this, '\\mathrm', 'span', 'class="mq-roman mq-font"', 'Roman Font', true); + super_.init.call(this, '\\mathrm', 'span', 'class="mq-roman mq-font"', 'Roman Font', { shouldNotSpeakDelimiters: true }); }; _.isTextBlock = function() { return true; From 23b0dd6e881fe22c09c18db4f442ffacb0bcae59 Mon Sep 17 00:00:00 2001 From: Jason Merrill Date: Mon, 24 May 2021 15:46:30 -0700 Subject: [PATCH 373/393] Writing -> converts to \to arrow --- src/commands/math/advancedSymbols.js | 2 -- src/commands/math/basicSymbols.js | 38 +++++++++++++++++++++++++++- test/unit/typing.test.js | 28 ++++++++++++++++++++ 3 files changed, 65 insertions(+), 3 deletions(-) diff --git a/src/commands/math/advancedSymbols.js b/src/commands/math/advancedSymbols.js index a1fe98dd1..65fa6352f 100644 --- a/src/commands/math/advancedSymbols.js +++ b/src/commands/math/advancedSymbols.js @@ -255,8 +255,6 @@ LatexCmds.diverges = LatexCmds.uarr = LatexCmds.uparrow = LatexCmds.uArr = LatexCmds.Uparrow = bind(VanillaSymbol,'\\Uparrow ','⇑', 'up arrow'); -LatexCmds.to = bind(BinaryOperator,'\\to ','→', 'to'); - LatexCmds.rarr = LatexCmds.rightarrow = bind(VanillaSymbol,'\\rightarrow ','→', 'right arrow'); LatexCmds.implies = bind(BinaryOperator,'\\Rightarrow ','⇒', 'implies'); diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index 9f1c3058b..c2c609d34 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -852,6 +852,25 @@ LatexCmds.mp = LatexCmds.mnplus = LatexCmds.minusplus = CharCmds['*'] = LatexCmds.sdot = LatexCmds.cdot = bind(BinaryOperator, '\\cdot ', '·', '*', 'times'); //semantically should be ⋅, but · looks better +var To = P(BinaryOperator, function(_, super_) { + _.init = function() { + super_.init.call(this, '\\to ','→', 'to'); + } + _.deleteTowards = function(dir, cursor) { + if (dir === L) { + var l = cursor[L]; + Fragment(l, this).remove(); + cursor[L] = l[L]; + LatexCmds['−']().createLeftOf(cursor); + cursor[L].bubble(function (node) { node.reflow(); }); + return; + } + super_.deleteTowards.apply(this, arguments); + }; +}) + +LatexCmds.to = To; + var Inequality = P(BinaryOperator, function(_, super_) { _.init = function(data, strict) { this.data = data; @@ -883,8 +902,25 @@ var less = { ctrlSeq: '\\le ', html: '≤', text: '≤', mathspeak: 'less than var greater = { 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); + }; + _.createLeftOf = function(cursor) { + if (cursor[L] instanceof BinaryOperator && cursor[L].ctrlSeq === '-') { + var l = cursor[L]; + cursor[L] = l[L]; + l.remove(); + To().createLeftOf(cursor); + cursor[L].bubble(function (node) { node.reflow(); }); + return; + } + super_.createLeftOf.apply(this, arguments); + }; +}) + LatexCmds['<'] = LatexCmds.lt = bind(Inequality, less, true); -LatexCmds['>'] = LatexCmds.gt = bind(Inequality, greater, true); +LatexCmds['>'] = LatexCmds.gt = Greater; LatexCmds['≤'] = LatexCmds.le = LatexCmds.leq = bind(Inequality, less, false); LatexCmds['≥'] = LatexCmds.ge = LatexCmds.geq = bind(Inequality, greater, false); LatexCmds.infty = LatexCmds.infin = LatexCmds.infinity = diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index c7c081816..cd4d992ed 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -1291,6 +1291,34 @@ suite('typing with auto-replaces', function() { assertFullyFunctioningInequality('\\ge', '>', 'greater than or equal to', 'greater than'); }); + test('typing and backspacing \\to', function() { + mq.typedText('-'); + assertLatex('-'); + assertMathspeak('negative'); + mq.typedText('>'); + assertLatex('\\to'); + assertMathspeak('to'); + mq.typedText('-'); + assertLatex('\\to-'); + assertMathspeak('to negative'); + mq.typedText('>'); + assertLatex('\\to\\to'); + assertMathspeak('to to'); + mq.keystroke('Backspace'); + assertLatex('\\to-'); + assertMathspeak('to negative'); + mq.keystroke('Backspace'); + assertLatex('\\to'); + assertMathspeak('to'); + mq.keystroke('Backspace'); + assertLatex('-'); + assertMathspeak('negative'); + mq.keystroke('Backspace'); + mq.typedText('a->b'); + assertLatex('a\\to b'); + assertMathspeak('"a" to "b"'); + }); + test('typing and backspacing ~', function() { mq.typedText('~'); assertLatex('\\sim'); From 6c9feed58e683814ee3a0fb7445e3ba72bcb278a Mon Sep 17 00:00:00 2001 From: Jason Merrill Date: Fri, 27 Aug 2021 13:23:35 -0400 Subject: [PATCH 374/393] =?UTF-8?q?Interpret=20=E2=86=92=20as=20\to=20when?= =?UTF-8?q?=20it=20is=20pasted=20in?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commands/math/basicSymbols.js | 2 +- test/unit/typing.test.js | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js index c2c609d34..45b16c62d 100644 --- a/src/commands/math/basicSymbols.js +++ b/src/commands/math/basicSymbols.js @@ -869,7 +869,7 @@ var To = P(BinaryOperator, function(_, super_) { }; }) -LatexCmds.to = To; +LatexCmds['→'] = LatexCmds.to = To; var Inequality = P(BinaryOperator, function(_, super_) { _.init = function(data, strict) { diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index cd4d992ed..d4480206a 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -1317,6 +1317,10 @@ suite('typing with auto-replaces', function() { mq.typedText('a->b'); assertLatex('a\\to b'); assertMathspeak('"a" to "b"'); + mq.latex(''); + mq.typedText('a→b'); + assertLatex('a\\to b'); + assertMathspeak('"a" to "b"'); }); test('typing and backspacing ~', function() { From ab881d0e7482b948d192568b6052edf0b8dcde22 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Mon, 30 Aug 2021 19:57:41 -0400 Subject: [PATCH 375/393] Consolidate ARIA label update code and use hidden span for static math --- src/commands/math.js | 5 ++--- src/css/mixins/display.less | 6 +++--- src/services/focusBlur.js | 14 ++++--------- src/services/latex.js | 7 ++----- src/services/textarea.js | 42 +++++++++++++++++-------------------- 5 files changed, 30 insertions(+), 44 deletions(-) diff --git a/src/commands/math.js b/src/commands/math.js index 50ce7781d..4b1dfd317 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -529,14 +529,13 @@ API.StaticMath = function(APIClasses) { node.registerInnerField(innerFields, APIClasses.MathField); }); // Force an ARIA label update to remain in sync with the new LaTeX value. - this.setAriaLabel(this.__controller.ariaLabel); + this.__controller.updateMathspeak(); } return returned; }; _.setAriaLabel = function(ariaLabel) { this.__controller.ariaLabel = typeof ariaLabel === 'string' ? ariaLabel : ''; - var prependedLabel = this.__controller.ariaLabel !== 'Math Input' ? this.__controller.ariaLabel + ': ' : ''; - this.__controller.container.attr('aria-label', prependedLabel + this.__controller.root.mathspeak().trim()); + this.updateMathspeak(); return this; }; _.getAriaLabel = function () { diff --git a/src/css/mixins/display.less b/src/css/mixins/display.less index ec58d1836..c80336741 100644 --- a/src/css/mixins/display.less +++ b/src/css/mixins/display.less @@ -4,12 +4,12 @@ } // ARIA alert styling; must technically be visible for browsers to fire needed events (except IE). Common technique is to show them offscreen so visual users aren't impacted. -.mq-aria-alert { +.mq-aria-alert, .mq-mathspeak { position: absolute; left: -1000px; top: -1000px; - width: 1px; - height: 1px; + width: 0px; + height: 0px; text-align: left; overflow: hidden; } diff --git a/src/services/focusBlur.js b/src/services/focusBlur.js index 1b78da0c9..093d2a152 100644 --- a/src/services/focusBlur.js +++ b/src/services/focusBlur.js @@ -32,7 +32,7 @@ Controller.open(function(_) { var ctrlr = this, root = ctrlr.root, cursor = ctrlr.cursor; var blurTimeout; ctrlr.textarea.focus(function() { - updateAria(); + ctrlr.updateMathspeak(); ctrlr.blurred = false; clearTimeout(blurTimeout); ctrlr.container.addClass('mq-focused'); @@ -56,7 +56,7 @@ Controller.open(function(_) { root.postOrder(function (node) { node.intentionalBlur(); }); // none, intentional blur: #264 cursor.clearSelection().endSelection(); blur(); - updateAria(); + ctrlr.updateMathspeak(); ctrlr.scrollHoriz(); }); $(window).bind('blur', windowBlur); @@ -65,7 +65,7 @@ Controller.open(function(_) { clearTimeout(blurTimeout); // tabs/windows, not intentional blur if (cursor.selection) cursor.selection.jQ.addClass('mq-blur'); blur(); - updateAria(); + 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 @@ -76,13 +76,7 @@ Controller.open(function(_) { cursor.resetToEnd(ctrlr); } } - function updateAria() { - var mqAria = (ctrlr.ariaLabel+': ' + root.mathspeak() + ' ' + ctrlr.ariaPostLabel).trim(); - aria.jQ.empty(); - ctrlr.textarea.attr('aria-label', mqAria); - ctrlr.container.attr('aria-label', mqAria); - } - updateAria(); + ctrlr.updateMathspeak(); ctrlr.blurred = true; cursor.hide().parent.blur(); }; diff --git a/src/services/latex.js b/src/services/latex.js index 4495ec9cc..e177a8b52 100644 --- a/src/services/latex.js +++ b/src/services/latex.js @@ -286,13 +286,10 @@ Controller.open(function(_, super_) { jQ.html(html); root.jQize(jQ.children()); root.finalizeInsert(cursor.options); - } - else { + } else { jQ.empty(); } - var prependedLabel = this.ariaLabel && this.ariaLabel !== 'Math Input' ? this.ariaLabel + ': ' : ''; - this.container.attr('aria-label', prependedLabel + root.mathspeak().trim()); - + this.updateMathspeak(); delete cursor.selection; cursor.insAtRightEnd(root); }; diff --git a/src/services/textarea.js b/src/services/textarea.js index d31e8efd0..65ff83e08 100644 --- a/src/services/textarea.js +++ b/src/services/textarea.js @@ -14,6 +14,7 @@ Controller.open(function(_) { if (!textarea.nodeType) { throw 'substituteTextarea() must return a DOM element, got ' + textarea; } + var mathspeakSpan = this.mathspeakSpan = $('').appendTo(textareaSpan); textarea = this.textarea = $(textarea).appendTo(textareaSpan); var ctrlr = this; @@ -47,7 +48,7 @@ Controller.open(function(_) { this.selectFn(latex); }; _.staticMathTextareaEvents = function() { - var ctrlr = this, root = ctrlr.root, cursor = ctrlr.cursor, + var ctrlr = this, cursor = ctrlr.cursor, textarea = ctrlr.textarea, textareaSpan = ctrlr.textareaSpan; this.container.prepend(''); @@ -71,28 +72,7 @@ Controller.open(function(_) { textarea.val(text); if (text) textarea.select(); }; - var ariaLabel = ctrlr && ctrlr.ariaLabel !== 'Math Input' ? ctrlr.ariaLabel + ': ' : ''; - ctrlr.container.attr('aria-label', ariaLabel + root.mathspeak().trim()); - - // This is gross, but necessary. - // On Windows, ChromeOS, Android, and iOS, supplying role="math" allows users of - // JAWS, NVDA, ChromeVox, Talkback, and VoiceOver to read the mathspeak version of an equation - // which we supply as the container's aria-label attribute. - // Omitting role="math" makes the container invisible to JAWS and iOS VoiceOver. - // At time of writing (8/20/2019), the exact opposite is true of the Mac-- - // Supplying role="math" makes the content of the container invisible to VoiceOver there, - // and omitting it makes the material available to Mac users. - // For now, the solution is to render role="math" unless the user is on Mac. - // Bug report: https://feedbackassistant.apple.com/feedback/7076111 - // Update: As of 11/23/2020, this problem becomes slightly more complicated now that iOS 13+ on iPads masquerades as a Mac. - // The same work-around applies, but now we must detect a spoofed Mac. - // Technique based on https://stackoverflow.com/questions/57765958/how-to-detect-ipad-and-ipad-os-version-in-ios-13-and-up - - var userAgent = navigator.userAgent || navigator.vendor || window.opera; - var isIOS = (/iPad|iPhone|iPod/.test(userAgent) || (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)) && !window.Stream; - var isMac = navigator.appVersion.indexOf("Mac") !== -1 && !isIOS; - if (!isMac) - ctrlr.container.attr('role', 'math'); + this.updateMathspeak(); }; Options.p.substituteKeyboardEvents = saneKeyboardEvents; _.editablesTextareaEvents = function() { @@ -145,4 +125,20 @@ Controller.open(function(_) { this.options.onPaste(); } }; + _.updateMathspeak = function() { + var ctrlr = this, root = ctrlr.root; + var mqAria = (ctrlr.ariaLabel+': ' + root.mathspeak() + ' ' + ctrlr.ariaPostLabel).trim(); + aria.jQ.empty(); + // For static math, provide mathspeak in a visually hidden span to allow screen readers and other AT to traverse the content. + // For editable math, assign the mathspeak to the container's ARIA label (AT can use text navigation to interrogate the content). + if (!!ctrlr.editable) { + this.mathspeakSpan.text(mqAria); + } else if (!ctrlr.textarea.attr('aria-hidden')) { + // The textarea is available to the screen reader. Set the label there. + ctrlr.textarea.attr('aria-label', mqAria); + } else { + // The textarea is hidden, so put the label on the container. + ctrlr.container.attr('aria-label', mqAria); + } + }; }); From 0580f2a0733f613c24d913e0cf4f826af9d9f2f0 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Wed, 1 Sep 2021 12:07:13 -0400 Subject: [PATCH 376/393] Tweak centralized updateMathspeak method --- src/controller.js | 2 +- src/publicapi.js | 4 ++-- src/services/focusBlur.js | 1 - src/services/textarea.js | 26 +++++++++++++++----------- 4 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/controller.js b/src/controller.js index 5138a8759..499229c43 100644 --- a/src/controller.js +++ b/src/controller.js @@ -15,7 +15,7 @@ var Controller = P(function(_) { this.container = container; this.options = options; - this.ariaLabel = 'Math Input'; + this.ariaLabel = 'Math Input:'; this.ariaPostLabel = ''; root.controller = this; diff --git a/src/publicapi.js b/src/publicapi.js index 94f101017..429aa87b3 100644 --- a/src/publicapi.js +++ b/src/publicapi.js @@ -223,11 +223,11 @@ function getInterface(v) { }; _.setAriaLabel = function(ariaLabel) { if(ariaLabel && typeof ariaLabel === 'string' && ariaLabel!='') this.__controller.ariaLabel = ariaLabel; - else this.__controller.ariaLabel = 'Math Input'; + else this.__controller.ariaLabel = 'Math Input:'; return this; }; _.getAriaLabel = function () { - return this.__controller.ariaLabel || 'Math Input'; + return this.__controller.ariaLabel || 'Math Input:'; }; _.setAriaPostLabel = function(ariaPostLabel, timeout) { var controller = this.__controller; diff --git a/src/services/focusBlur.js b/src/services/focusBlur.js index 093d2a152..f69ecef15 100644 --- a/src/services/focusBlur.js +++ b/src/services/focusBlur.js @@ -76,7 +76,6 @@ Controller.open(function(_) { cursor.resetToEnd(ctrlr); } } - ctrlr.updateMathspeak(); ctrlr.blurred = true; cursor.hide().parent.blur(); }; diff --git a/src/services/textarea.js b/src/services/textarea.js index 65ff83e08..6c28bc0ab 100644 --- a/src/services/textarea.js +++ b/src/services/textarea.js @@ -14,7 +14,6 @@ Controller.open(function(_) { if (!textarea.nodeType) { throw 'substituteTextarea() must return a DOM element, got ' + textarea; } - var mathspeakSpan = this.mathspeakSpan = $('').appendTo(textareaSpan); textarea = this.textarea = $(textarea).appendTo(textareaSpan); var ctrlr = this; @@ -51,6 +50,8 @@ Controller.open(function(_) { var ctrlr = this, cursor = ctrlr.cursor, textarea = ctrlr.textarea, textareaSpan = ctrlr.textareaSpan; + this.mathspeakSpan = $(''); + this.container.prepend(this.mathspeakSpan); this.container.prepend(''); ctrlr.blurred = true; textarea.bind('cut paste', false); @@ -82,6 +83,7 @@ Controller.open(function(_) { this.selectFn = function(text) { keyboardEventsShim.select(text); }; this.container.prepend(textareaSpan); this.focusBlurEvents(); + this.updateMathspeak(); }; _.typedText = function(ch) { if (ch === '\n') return this.handle('enter'); @@ -126,19 +128,21 @@ Controller.open(function(_) { } }; _.updateMathspeak = function() { - var ctrlr = this, root = ctrlr.root; - var mqAria = (ctrlr.ariaLabel+': ' + root.mathspeak() + ' ' + ctrlr.ariaPostLabel).trim(); + var ctrlr = this; + var mathspeak = ctrlr.root.mathspeak(); aria.jQ.empty(); // For static math, provide mathspeak in a visually hidden span to allow screen readers and other AT to traverse the content. - // For editable math, assign the mathspeak to the container's ARIA label (AT can use text navigation to interrogate the content). - if (!!ctrlr.editable) { - this.mathspeakSpan.text(mqAria); - } else if (!ctrlr.textarea.attr('aria-hidden')) { - // The textarea is available to the screen reader. Set the label there. - ctrlr.textarea.attr('aria-label', mqAria); + // For editable math, assign the mathspeak to the textarea's ARIA label (AT can use text navigation to interrogate the content). + // Be certain to include the mathspeak for only one of these, though, as we don't want to include outdated labels if a field's editable state changes. + // The mathspeakSpan should exist only for static math, so we use its presence to decide which approach to take. + if (!!ctrlr.mathspeakSpan) { + ctrlr.textarea.attr('aria-label', ''); + ctrlr.mathspeakSpan.text(mathspeak); } else { - // The textarea is hidden, so put the label on the container. - ctrlr.container.attr('aria-label', mqAria); + ctrlr.textarea.attr( + 'aria-label', + (ctrlr.ariaLabel+' ' + mathspeak + ' ' + ctrlr.ariaPostLabel).trim() + ); } }; }); From aa6bc8b344cb31f02afc99b7cfd00e8ddeff46dd Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Wed, 1 Sep 2021 12:43:32 -0400 Subject: [PATCH 377/393] Update behavior and tests --- src/commands/math.js | 2 +- src/services/textarea.js | 8 ++++++-- test/unit/aria.test.js | 18 +++++++++--------- test/unit/publicapi.test.js | 2 +- 4 files changed, 17 insertions(+), 13 deletions(-) diff --git a/src/commands/math.js b/src/commands/math.js index 4b1dfd317..2fba72c3d 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -535,7 +535,7 @@ API.StaticMath = function(APIClasses) { }; _.setAriaLabel = function(ariaLabel) { this.__controller.ariaLabel = typeof ariaLabel === 'string' ? ariaLabel : ''; - this.updateMathspeak(); + this.__controller.updateMathspeak(); return this; }; _.getAriaLabel = function () { diff --git a/src/services/textarea.js b/src/services/textarea.js index 6c28bc0ab..19bd25c8c 100644 --- a/src/services/textarea.js +++ b/src/services/textarea.js @@ -129,15 +129,19 @@ Controller.open(function(_) { }; _.updateMathspeak = function() { var ctrlr = this; - var mathspeak = ctrlr.root.mathspeak(); + var mathspeak = ctrlr.root.mathspeak().trim(); aria.jQ.empty(); // For static math, provide mathspeak in a visually hidden span to allow screen readers and other AT to traverse the content. // For editable math, assign the mathspeak to the textarea's ARIA label (AT can use text navigation to interrogate the content). // Be certain to include the mathspeak for only one of these, though, as we don't want to include outdated labels if a field's editable state changes. // The mathspeakSpan should exist only for static math, so we use its presence to decide which approach to take. if (!!ctrlr.mathspeakSpan) { + var newLabel = + ctrlr.ariaLabel.length > 0 && ctrlr.ariaLabel !== 'Math Input:' + ? (ctrlr.ariaLabel+' ' + mathspeak).trim() + : mathspeak; ctrlr.textarea.attr('aria-label', ''); - ctrlr.mathspeakSpan.text(mathspeak); + ctrlr.mathspeakSpan.text(newLabel); } else { ctrlr.textarea.attr( 'aria-label', diff --git a/test/unit/aria.test.js b/test/unit/aria.test.js index 80385e50d..fc3843624 100644 --- a/test/unit/aria.test.js +++ b/test/unit/aria.test.js @@ -100,26 +100,26 @@ suite('aria', function() { mathField.keystroke('End'); assertAriaEqual('end of block "s" "q" "r" "t" left parenthesis, "x" , right parenthesis'); mathField.keystroke('Ctrl-Home'); - assertAriaEqual('beginning of Math Input "s" "q" "r" "t" left parenthesis, "x" , right parenthesis'); + assertAriaEqual('beginning of Math Input: "s" "q" "r" "t" left parenthesis, "x" , right parenthesis'); mathField.keystroke('Ctrl-End'); - assertAriaEqual('end of Math Input "s" "q" "r" "t" left parenthesis, "x" , right parenthesis'); + assertAriaEqual('end of Math Input: "s" "q" "r" "t" left parenthesis, "x" , right parenthesis'); }); test('testing aria-label for interactive and static math', function(done) { mathField.typedText('sqrt(x)'); mathField.blur(); setTimeout(function() { - assert.equal(mathField.__controller.container.attr('aria-label'), 'Math Input: "s" "q" "r" "t" left parenthesis, "x" , right parenthesis'); + assert.equal(mathField.__controller.textarea.attr('aria-label'), 'Math Input: "s" "q" "r" "t" left parenthesis, "x" , right parenthesis'); done(); }); var staticMath = MQ.StaticMath($('y=\\frac{2x}{3y}').appendTo('#mock')[0]); - assert.equal('"y" equals StartFraction, 2 "x" Over 3 "y" , EndFraction', staticMath.__controller.container.attr('aria-label')); - assert.equal('Math Input', staticMath.getAriaLabel()); - staticMath.setAriaLabel('Static Label'); - assert.equal('Static Label: "y" equals StartFraction, 2 "x" Over 3 "y" , EndFraction', staticMath.__controller.container.attr('aria-label')); - assert.equal('Static Label', staticMath.getAriaLabel()); + assert.equal('"y" equals StartFraction, 2 "x" Over 3 "y" , EndFraction', staticMath.__controller.mathspeakSpan.text()); + assert.equal('Math Input:', staticMath.getAriaLabel()); + staticMath.setAriaLabel('Static Label:'); + assert.equal('Static Label: "y" equals StartFraction, 2 "x" Over 3 "y" , EndFraction', staticMath.__controller.mathspeakSpan.text()); + assert.equal('Static Label:', staticMath.getAriaLabel()); staticMath.latex('2+2'); - assert.equal('Static Label: 2 plus 2', staticMath.__controller.container.attr('aria-label')); + assert.equal('Static Label: 2 plus 2', staticMath.__controller.mathspeakSpan.text()); }); }); diff --git a/test/unit/publicapi.test.js b/test/unit/publicapi.test.js index c99f38164..df49b0a46 100644 --- a/test/unit/publicapi.test.js +++ b/test/unit/publicapi.test.js @@ -165,7 +165,7 @@ suite('Public API', function() { assert.equal(mq.getAriaPostLabel(), 'ARIA post-label'); mq.setAriaLabel(''); mq.setAriaPostLabel(''); - assert.equal(mq.getAriaLabel(), 'Math Input'); + assert.equal(mq.getAriaLabel(), 'Math Input:'); assert.equal(mq.getAriaPostLabel(), ''); }); From eddf1c2dd67ff02b55657b08b97a23f880e5cc94 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 7 Sep 2021 12:15:14 -0400 Subject: [PATCH 378/393] Tweak some things --- src/css/mixins/display.less | 2 +- src/services/textarea.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/css/mixins/display.less b/src/css/mixins/display.less index c80336741..fdc5d6d7d 100644 --- a/src/css/mixins/display.less +++ b/src/css/mixins/display.less @@ -4,7 +4,7 @@ } // ARIA alert styling; must technically be visible for browsers to fire needed events (except IE). Common technique is to show them offscreen so visual users aren't impacted. -.mq-aria-alert, .mq-mathspeak { +.mq-aria-alert .mq-mathspeak { position: absolute; left: -1000px; top: -1000px; diff --git a/src/services/textarea.js b/src/services/textarea.js index 19bd25c8c..aaefc2acb 100644 --- a/src/services/textarea.js +++ b/src/services/textarea.js @@ -50,9 +50,9 @@ Controller.open(function(_) { var ctrlr = this, cursor = ctrlr.cursor, textarea = ctrlr.textarea, textareaSpan = ctrlr.textareaSpan; + this.container.prepend(''); this.mathspeakSpan = $(''); this.container.prepend(this.mathspeakSpan); - this.container.prepend(''); ctrlr.blurred = true; textarea.bind('cut paste', false); if (ctrlr.options.disableCopyPaste) { From 3101dbb8f1c30e433856178f947a0754801a4265 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 7 Sep 2021 13:46:53 -0400 Subject: [PATCH 379/393] More CSS twaking --- src/css/mixins/display.less | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/css/mixins/display.less b/src/css/mixins/display.less index fdc5d6d7d..0d1a9b9b4 100644 --- a/src/css/mixins/display.less +++ b/src/css/mixins/display.less @@ -4,7 +4,17 @@ } // ARIA alert styling; must technically be visible for browsers to fire needed events (except IE). Common technique is to show them offscreen so visual users aren't impacted. -.mq-aria-alert .mq-mathspeak { +.mq-aria-alert { + position: absolute; + left: -1000px; + top: -1000px; + width: 0px; + height: 0px; + text-align: left; + overflow: hidden; +} + +.mq-mathspeak { position: absolute; left: -1000px; top: -1000px; From 37a7a4527e7215d066bf34360563543bbbaf0e57 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 7 Sep 2021 16:49:25 -0400 Subject: [PATCH 380/393] Centralize ARIA label functions to controller --- src/commands/math.js | 7 +++++-- src/controller.js | 36 ++++++++++++++++++++++++++++++++++++ src/publicapi.js | 26 ++++---------------------- test/unit/aria.test.js | 4 ++-- test/unit/publicapi.test.js | 4 +++- 5 files changed, 50 insertions(+), 27 deletions(-) diff --git a/src/commands/math.js b/src/commands/math.js index 2fba72c3d..f8083b3be 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -534,12 +534,15 @@ API.StaticMath = function(APIClasses) { return returned; }; _.setAriaLabel = function(ariaLabel) { - this.__controller.ariaLabel = typeof ariaLabel === 'string' ? ariaLabel : ''; + this.__controller.setAriaLabel(ariaLabel); this.__controller.updateMathspeak(); return this; }; _.getAriaLabel = function () { - return this.__controller.ariaLabel || ''; + var returnedLabel = this.__controller.getAriaLabel(); + // For static math, ignore the default "Math Input:" label. + if (returnedLabel === 'Math Input:') return ''; + return returnedLabel; }; }); }; diff --git a/src/controller.js b/src/controller.js index 499229c43..87bc98747 100644 --- a/src/controller.js +++ b/src/controller.js @@ -41,4 +41,40 @@ var Controller = P(function(_) { } return this; }; + _.setAriaLabel = function(ariaLabel) { + if (ariaLabel && typeof ariaLabel === 'string' && ariaLabel !== '') { + // If the supplied label doesn't end with a punctuation mark, add a colon by default. + var suffix = /[\d\l]$/.test(ariaLabel) ? ':' : ''; + this.ariaLabel = ariaLabel + suffix; + } else { + this.ariaLabel = 'Math Input:'; + } + return this; + }; + _.getAriaLabel = function () { + return this.ariaLabel || 'Math Input:'; + }; + _.setAriaPostLabel = function(ariaPostLabel, timeout) { + if(ariaPostLabel && typeof ariaPostLabel === 'string' && ariaPostLabel!='') { + if ( + ariaPostLabel !== this.ariaPostLabel && + typeof timeout === 'number' + ) { + if (this._ariaAlertTimeout) clearTimeout(this._ariaAlertTimeout); + this._ariaAlertTimeout = setTimeout(function() { + if (!!$(document.activeElement).closest($(this.container)).length) { + aria.alert(this.root.mathspeak().trim() + ' ' + ariaPostLabel.trim()); + } + }.bind(this), timeout); + } + this.ariaPostLabel = ariaPostLabel; + } else { + if (this._ariaAlertTimeout) clearTimeout(this._ariaAlertTimeout); + this.ariaPostLabel = ''; + } + return this; + }; + _.getAriaPostLabel = function () { + return this.ariaPostLabel || ''; + }; }); diff --git a/src/publicapi.js b/src/publicapi.js index 429aa87b3..66d7d6ce5 100644 --- a/src/publicapi.js +++ b/src/publicapi.js @@ -222,36 +222,18 @@ function getInterface(v) { cmd.createLeftOf(this.__controller.cursor); }; _.setAriaLabel = function(ariaLabel) { - if(ariaLabel && typeof ariaLabel === 'string' && ariaLabel!='') this.__controller.ariaLabel = ariaLabel; - else this.__controller.ariaLabel = 'Math Input:'; + this.__controller.setAriaLabel(ariaLabel); return this; }; _.getAriaLabel = function () { - return this.__controller.ariaLabel || 'Math Input:'; + return this.__controller.getAriaLabel(); }; _.setAriaPostLabel = function(ariaPostLabel, timeout) { - var controller = this.__controller; - if(ariaPostLabel && typeof ariaPostLabel === 'string' && ariaPostLabel!='') { - if ( - ariaPostLabel !== controller.ariaPostLabel && - typeof timeout === 'number' - ) { - if (this.ariaAlertTimeout) clearTimeout(this.ariaAlertTimeout); - this.ariaAlertTimeout = setTimeout(function() { - if (!!$(document.activeElement).closest($(controller.container)).length) { - aria.alert(this.mathspeak().trim() + ' ' + ariaPostLabel.trim()); - } - }.bind(this), timeout); - } - controller.ariaPostLabel = ariaPostLabel; - } else { - if (this.ariaAlertTimeout) clearTimeout(this.ariaAlertTimeout); - controller.ariaPostLabel = ''; - } + this.__controller.setAriaPostLabel(ariaPostLabel, timeout); return this; }; _.getAriaPostLabel = function () { - return this.__controller.ariaPostLabel || ''; + return this.__controller.getAriaPostLabel(); }; _.clickAt = function(clientX, clientY, target) { target = target || document.elementFromPoint(clientX, clientY); diff --git a/test/unit/aria.test.js b/test/unit/aria.test.js index fc3843624..13d41b98c 100644 --- a/test/unit/aria.test.js +++ b/test/unit/aria.test.js @@ -114,8 +114,8 @@ suite('aria', function() { }); var staticMath = MQ.StaticMath($('y=\\frac{2x}{3y}').appendTo('#mock')[0]); assert.equal('"y" equals StartFraction, 2 "x" Over 3 "y" , EndFraction', staticMath.__controller.mathspeakSpan.text()); - assert.equal('Math Input:', staticMath.getAriaLabel()); - staticMath.setAriaLabel('Static Label:'); + assert.equal('', staticMath.getAriaLabel()); + staticMath.setAriaLabel('Static Label'); assert.equal('Static Label: "y" equals StartFraction, 2 "x" Over 3 "y" , EndFraction', staticMath.__controller.mathspeakSpan.text()); assert.equal('Static Label:', staticMath.getAriaLabel()); staticMath.latex('2+2'); diff --git a/test/unit/publicapi.test.js b/test/unit/publicapi.test.js index df49b0a46..2e4d9e646 100644 --- a/test/unit/publicapi.test.js +++ b/test/unit/publicapi.test.js @@ -161,12 +161,14 @@ suite('Public API', function() { test('ARIA labels', function() { mq.setAriaLabel('ARIA label'); mq.setAriaPostLabel('ARIA post-label'); - assert.equal(mq.getAriaLabel(), 'ARIA label'); + assert.equal(mq.getAriaLabel(), 'ARIA label:'); assert.equal(mq.getAriaPostLabel(), 'ARIA post-label'); mq.setAriaLabel(''); mq.setAriaPostLabel(''); assert.equal(mq.getAriaLabel(), 'Math Input:'); assert.equal(mq.getAriaPostLabel(), ''); + mq.setAriaLabel('Another label:'); + assert.equal(mq.getAriaLabel(), 'Another label:'); }); test('.mathspeak()', function() { From c34dad37185db8d5e91353e6da47e8ef407d916a Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Wed, 8 Sep 2021 16:12:08 -0400 Subject: [PATCH 381/393] More ARIA label tweaking * Call updateMathspeak from the primary setAriaLabel() method if the active Mathquill element does not have focus (this was previously called in textarea, but wouldn't necessarily fire if an outsider was setting the label via the public API). * Add the colon symbol only when computing an aria-label + mathspeak for the active Mathquill-- should restore previously expected behavior. --- src/commands/math.js | 6 +----- src/controller.js | 35 ++++++++++++++++++++++++++++------- src/services/textarea.js | 16 ++++++++++------ test/unit/aria.test.js | 6 +++--- test/unit/publicapi.test.js | 6 ++---- 5 files changed, 44 insertions(+), 25 deletions(-) diff --git a/src/commands/math.js b/src/commands/math.js index f8083b3be..f28914f9d 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -535,14 +535,10 @@ API.StaticMath = function(APIClasses) { }; _.setAriaLabel = function(ariaLabel) { this.__controller.setAriaLabel(ariaLabel); - this.__controller.updateMathspeak(); return this; }; _.getAriaLabel = function () { - var returnedLabel = this.__controller.getAriaLabel(); - // For static math, ignore the default "Math Input:" label. - if (returnedLabel === 'Math Input:') return ''; - return returnedLabel; + return this.__controller.getAriaLabel(); }; }); }; diff --git a/src/controller.js b/src/controller.js index 87bc98747..994194ab1 100644 --- a/src/controller.js +++ b/src/controller.js @@ -15,7 +15,7 @@ var Controller = P(function(_) { this.container = container; this.options = options; - this.ariaLabel = 'Math Input:'; + this.ariaLabel = 'Math Input'; this.ariaPostLabel = ''; root.controller = this; @@ -43,16 +43,29 @@ var Controller = P(function(_) { }; _.setAriaLabel = function(ariaLabel) { if (ariaLabel && typeof ariaLabel === 'string' && ariaLabel !== '') { - // If the supplied label doesn't end with a punctuation mark, add a colon by default. - var suffix = /[\d\l]$/.test(ariaLabel) ? ':' : ''; - this.ariaLabel = ariaLabel + suffix; + this.ariaLabel = ariaLabel; + } else if (this.editable) { + this.ariaLabel = 'Math Input'; } else { - this.ariaLabel = 'Math Input:'; + this.ariaLabel = ''; + } + // If this field doesn't have focus, update its computed mathspeak value. + // We check for focus because updating the aria-label attribute of a focused element will cause most screen readers to announce the new value (in our case, label along with the expression's mathspeak). + // If the field does have focus at the time, it will be updated once a blur event occurs. + // Unless we stop using fake text inputs and emulating screen reader behavior, this is going to remain a problem. + if (!this.containerHasFocus()) { + this.updateMathspeak(); } return this; }; _.getAriaLabel = function () { - return this.ariaLabel || 'Math Input:'; + if (this.ariaLabel !== 'Math Input') { + return this.ariaLabel; + } else if (this.editable) { + return 'Math Input'; + } else { + return ''; + } }; _.setAriaPostLabel = function(ariaPostLabel, timeout) { if(ariaPostLabel && typeof ariaPostLabel === 'string' && ariaPostLabel!='') { @@ -62,7 +75,7 @@ var Controller = P(function(_) { ) { if (this._ariaAlertTimeout) clearTimeout(this._ariaAlertTimeout); this._ariaAlertTimeout = setTimeout(function() { - if (!!$(document.activeElement).closest($(this.container)).length) { + if (this.containerHasFocus()) { aria.alert(this.root.mathspeak().trim() + ' ' + ariaPostLabel.trim()); } }.bind(this), timeout); @@ -77,4 +90,12 @@ var Controller = P(function(_) { _.getAriaPostLabel = function () { return this.ariaPostLabel || ''; }; + _.containerHasFocus = function () { + return ( + document.activeElement && + this.container && + this.container[0] && + this.container[0].contains(document.activeElement) + ); + }; }); diff --git a/src/services/textarea.js b/src/services/textarea.js index aaefc2acb..2e33052de 100644 --- a/src/services/textarea.js +++ b/src/services/textarea.js @@ -129,23 +129,27 @@ Controller.open(function(_) { }; _.updateMathspeak = function() { var ctrlr = this; + // If the controller's ARIA label doesn't end with a punctuation mark, add a colon by default to better separate it from mathspeak. + var ariaLabel = ctrlr.getAriaLabel(); + var labelWithSuffix = + /[A-Za-z0-9]$/.test(ariaLabel) + ? ariaLabel + ':' + : ariaLabel; var mathspeak = ctrlr.root.mathspeak().trim(); aria.jQ.empty(); // For static math, provide mathspeak in a visually hidden span to allow screen readers and other AT to traverse the content. // For editable math, assign the mathspeak to the textarea's ARIA label (AT can use text navigation to interrogate the content). // Be certain to include the mathspeak for only one of these, though, as we don't want to include outdated labels if a field's editable state changes. + // By design, also take careful note that the ariaPostLabel is meant to exist only for editable math (e.g. to serve as an evaluation or error message) + // so it is not included for static math mathspeak calculations. // The mathspeakSpan should exist only for static math, so we use its presence to decide which approach to take. if (!!ctrlr.mathspeakSpan) { - var newLabel = - ctrlr.ariaLabel.length > 0 && ctrlr.ariaLabel !== 'Math Input:' - ? (ctrlr.ariaLabel+' ' + mathspeak).trim() - : mathspeak; ctrlr.textarea.attr('aria-label', ''); - ctrlr.mathspeakSpan.text(newLabel); + ctrlr.mathspeakSpan.text((labelWithSuffix+' ' + mathspeak).trim()); } else { ctrlr.textarea.attr( 'aria-label', - (ctrlr.ariaLabel+' ' + mathspeak + ' ' + ctrlr.ariaPostLabel).trim() + (labelWithSuffix+' ' + mathspeak + ' ' + ctrlr.ariaPostLabel).trim() ); } }; diff --git a/test/unit/aria.test.js b/test/unit/aria.test.js index 13d41b98c..adaff0a3a 100644 --- a/test/unit/aria.test.js +++ b/test/unit/aria.test.js @@ -100,9 +100,9 @@ suite('aria', function() { mathField.keystroke('End'); assertAriaEqual('end of block "s" "q" "r" "t" left parenthesis, "x" , right parenthesis'); mathField.keystroke('Ctrl-Home'); - assertAriaEqual('beginning of Math Input: "s" "q" "r" "t" left parenthesis, "x" , right parenthesis'); + assertAriaEqual('beginning of Math Input "s" "q" "r" "t" left parenthesis, "x" , right parenthesis'); mathField.keystroke('Ctrl-End'); - assertAriaEqual('end of Math Input: "s" "q" "r" "t" left parenthesis, "x" , right parenthesis'); + assertAriaEqual('end of Math Input "s" "q" "r" "t" left parenthesis, "x" , right parenthesis'); }); test('testing aria-label for interactive and static math', function(done) { @@ -117,7 +117,7 @@ suite('aria', function() { assert.equal('', staticMath.getAriaLabel()); staticMath.setAriaLabel('Static Label'); assert.equal('Static Label: "y" equals StartFraction, 2 "x" Over 3 "y" , EndFraction', staticMath.__controller.mathspeakSpan.text()); - assert.equal('Static Label:', staticMath.getAriaLabel()); + assert.equal('Static Label', staticMath.getAriaLabel()); staticMath.latex('2+2'); assert.equal('Static Label: 2 plus 2', staticMath.__controller.mathspeakSpan.text()); }); diff --git a/test/unit/publicapi.test.js b/test/unit/publicapi.test.js index 2e4d9e646..c99f38164 100644 --- a/test/unit/publicapi.test.js +++ b/test/unit/publicapi.test.js @@ -161,14 +161,12 @@ suite('Public API', function() { test('ARIA labels', function() { mq.setAriaLabel('ARIA label'); mq.setAriaPostLabel('ARIA post-label'); - assert.equal(mq.getAriaLabel(), 'ARIA label:'); + assert.equal(mq.getAriaLabel(), 'ARIA label'); assert.equal(mq.getAriaPostLabel(), 'ARIA post-label'); mq.setAriaLabel(''); mq.setAriaPostLabel(''); - assert.equal(mq.getAriaLabel(), 'Math Input:'); + assert.equal(mq.getAriaLabel(), 'Math Input'); assert.equal(mq.getAriaPostLabel(), ''); - mq.setAriaLabel('Another label:'); - assert.equal(mq.getAriaLabel(), 'Another label:'); }); test('.mathspeak()', function() { From 6ab1d05aa79ca695b8ad7cb9f7127af411021e83 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Fri, 10 Sep 2021 14:11:19 -0400 Subject: [PATCH 382/393] Fix whitespace --- src/controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/controller.js b/src/controller.js index 994194ab1..9c22f09cb 100644 --- a/src/controller.js +++ b/src/controller.js @@ -68,7 +68,7 @@ var Controller = P(function(_) { } }; _.setAriaPostLabel = function(ariaPostLabel, timeout) { - if(ariaPostLabel && typeof ariaPostLabel === 'string' && ariaPostLabel!='') { + if(ariaPostLabel && typeof ariaPostLabel === 'string' && ariaPostLabel !== '') { if ( ariaPostLabel !== this.ariaPostLabel && typeof timeout === 'number' From 1bb5e72e733bbc7cf5b8e5200d21056339df46ac Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Mon, 13 Sep 2021 14:31:30 -0400 Subject: [PATCH 383/393] Update math field's mathspeak when setting aria post-label if not focused --- src/controller.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/controller.js b/src/controller.js index 9c22f09cb..3d2b999f3 100644 --- a/src/controller.js +++ b/src/controller.js @@ -42,6 +42,7 @@ var Controller = P(function(_) { return this; }; _.setAriaLabel = function(ariaLabel) { + var oldAriaLabel = this.getAriaLabel(); if (ariaLabel && typeof ariaLabel === 'string' && ariaLabel !== '') { this.ariaLabel = ariaLabel; } else if (this.editable) { @@ -53,7 +54,7 @@ var Controller = P(function(_) { // We check for focus because updating the aria-label attribute of a focused element will cause most screen readers to announce the new value (in our case, label along with the expression's mathspeak). // If the field does have focus at the time, it will be updated once a blur event occurs. // Unless we stop using fake text inputs and emulating screen reader behavior, this is going to remain a problem. - if (!this.containerHasFocus()) { + if (this.ariaLabel !== oldAriaLabel && !this.containerHasFocus()) { this.updateMathspeak(); } return this; @@ -76,7 +77,11 @@ var Controller = P(function(_) { if (this._ariaAlertTimeout) clearTimeout(this._ariaAlertTimeout); this._ariaAlertTimeout = setTimeout(function() { 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()); + } else { + // This mathquill does not have focus, so update its mathspeak. + this.updateMathspeak(); } }.bind(this), timeout); } From cbe638f683e43f242d0f914389df902dea7a8295 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Mon, 13 Sep 2021 16:57:54 -0400 Subject: [PATCH 384/393] Check that old and new Latex values differ in efficient update --- src/services/latex.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/services/latex.js b/src/services/latex.js index 4495ec9cc..80406462d 100644 --- a/src/services/latex.js +++ b/src/services/latex.js @@ -118,10 +118,11 @@ Controller.open(function(_, super_) { } }; _.renderLatexMathEfficiently = function (latex) { - var oldLatex, oldClassification; + var oldLatex = this.exportLatex(); + if (latex === oldLatex) return true; + var oldClassification; var classification = this.classifyLatexForEfficientUpdate(latex); if (classification) { - oldLatex = this.exportLatex(); oldClassification = this.classifyLatexForEfficientUpdate(oldLatex); if (!oldClassification || oldClassification.prefix !== classification.prefix) { return false; From fbb55386253a0884042f6580717abfd615fe01fb Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Mon, 13 Sep 2021 17:09:40 -0400 Subject: [PATCH 385/393] Make Latex comparison only after the current block has been classified --- src/services/latex.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/latex.js b/src/services/latex.js index 80406462d..ac08dffaf 100644 --- a/src/services/latex.js +++ b/src/services/latex.js @@ -119,10 +119,10 @@ Controller.open(function(_, super_) { }; _.renderLatexMathEfficiently = function (latex) { var oldLatex = this.exportLatex(); - if (latex === oldLatex) return true; var oldClassification; var classification = this.classifyLatexForEfficientUpdate(latex); if (classification) { + if (oldLatex === latex) return true; oldClassification = this.classifyLatexForEfficientUpdate(oldLatex); if (!oldClassification || oldClassification.prefix !== classification.prefix) { return false; From 18f67923d33d3718a08cb6df370931cb7b384666 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 14 Sep 2021 12:33:16 -0400 Subject: [PATCH 386/393] Update latex short-circuit check to account for existing mathquill nodes Also updates tests. --- src/services/latex.js | 7 +++++-- test/unit/autoOperatorNames.test.js | 5 +++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/services/latex.js b/src/services/latex.js index ac08dffaf..f4ba0bb74 100644 --- a/src/services/latex.js +++ b/src/services/latex.js @@ -118,11 +118,14 @@ Controller.open(function(_, super_) { } }; _.renderLatexMathEfficiently = function (latex) { + var root = this.root; var oldLatex = this.exportLatex(); + if (root.ends[L] && root.ends[R] && oldLatex === latex) { + return true; + } var oldClassification; var classification = this.classifyLatexForEfficientUpdate(latex); if (classification) { - if (oldLatex === latex) return true; oldClassification = this.classifyLatexForEfficientUpdate(oldLatex); if (!oldClassification || oldClassification.prefix !== classification.prefix) { return false; @@ -131,7 +134,6 @@ Controller.open(function(_, super_) { return false; } - var root = this.root; // check if minus sign is changing var oldDigits = oldClassification.digits; @@ -268,6 +270,7 @@ Controller.open(function(_, super_) { return true; }; _.renderLatexMathFromScratch = function (latex) { + this.rendered = true; var root = this.root, cursor = this.cursor; var all = Parser.all; var eof = Parser.eof; diff --git a/test/unit/autoOperatorNames.test.js b/test/unit/autoOperatorNames.test.js index f68aeebce..969d55a11 100644 --- a/test/unit/autoOperatorNames.test.js +++ b/test/unit/autoOperatorNames.test.js @@ -34,14 +34,15 @@ suite('autoOperatorNames', function() { assertLatex('parsing \''+str+'\'', latex); assert.equal(count, 1); + // Since Latex doesn't change, count should remain at 1. mq.latex(latex); assertLatex('parsing \''+latex+'\'', latex); - assert.equal(count, 2); + assert.equal(count, 1); mq.latex(''); for (var i = 0; i < str.length; i += 1) mq.typedText(str.charAt(i)); assertLatex('typing \''+str+'\'', latex); - assert.equal(count, 2 + str.length); + assert.equal(count, 1 + str.length); } assertAutoOperatorNamesWork('sin', '\\sin'); From 3847d1f1ca57de4f66b4e21ffbfdf99d9ca0265a Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Tue, 14 Sep 2021 13:51:44 -0400 Subject: [PATCH 387/393] Remove debug line --- src/services/latex.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/services/latex.js b/src/services/latex.js index f4ba0bb74..34bb74c91 100644 --- a/src/services/latex.js +++ b/src/services/latex.js @@ -270,7 +270,6 @@ Controller.open(function(_, super_) { return true; }; _.renderLatexMathFromScratch = function (latex) { - this.rendered = true; var root = this.root, cursor = this.cursor; var all = Parser.all; var eof = Parser.eof; From d99a985e605d4c7e95a3d3205472ad5c04da8a8e Mon Sep 17 00:00:00 2001 From: Jason Merrill Date: Thu, 11 Nov 2021 18:57:20 -0500 Subject: [PATCH 388/393] Use native focus events insted of jQuery focus events jQuery 3.x has introduced a bunch of different focus bugs in the process of attempting to migrate to using native focus events in more cases, some of which remain unfixed in the latest version (3.6.0). Examples: * https://github.com/jquery/jquery/issues/4859 * https://github.com/jquery/jquery/issues/4856 * https://github.com/jquery/jquery/issues/4950 * https://github.com/jquery/jquery/issues/4867 Some unknown bug in this general class can make it so that Mathquill public api focus calls stop working because using jQuery to programatically trigger focus breaks. Work around this by switching to triggering native focus events instead of jQuery focus events. --- src/publicapi.js | 2 +- src/services/mouse.js | 2 +- src/services/saneKeyboardEvents.util.js | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/publicapi.js b/src/publicapi.js index 66d7d6ce5..70d9920ea 100644 --- a/src/publicapi.js +++ b/src/publicapi.js @@ -154,7 +154,7 @@ function getInterface(v) { return this; }; _.focus = function() { - this.__controller.textarea.focus(); + this.__controller.textarea[0].focus(); this.__controller.scrollHoriz(); return this; }; diff --git a/src/services/mouse.js b/src/services/mouse.js index dd6725850..b6d4b8b96 100644 --- a/src/services/mouse.js +++ b/src/services/mouse.js @@ -87,7 +87,7 @@ Controller.open(function(_) { if (ctrlr.blurred) { if (!ctrlr.editable) rootjQ.prepend(textareaSpan); - textarea.focus(); + textarea[0].focus(); // focus call may bubble to clients, who may then write to // mathquill, triggering cancelSelectionOnEdit. If that happens, we // don't want to stop the cursor blink or bind listeners, diff --git a/src/services/saneKeyboardEvents.util.js b/src/services/saneKeyboardEvents.util.js index 57dba0f92..7c32b99ac 100644 --- a/src/services/saneKeyboardEvents.util.js +++ b/src/services/saneKeyboardEvents.util.js @@ -282,7 +282,7 @@ var saneKeyboardEvents = (function() { // // And by nifty, we mean dumb (but useful sometimes). if (document.activeElement !== textarea[0]) { - textarea.focus(); + textarea[0].focus(); } checkTextareaFor(pastedText); From 77e3d2d702dd2949290e86883a939aa267e1d731 Mon Sep 17 00:00:00 2001 From: Jason Altekruse Date: Mon, 15 Nov 2021 17:45:27 -0600 Subject: [PATCH 389/393] Remove reference to deleted dispose() method --- src/publicapi.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/publicapi.js b/src/publicapi.js index 77502add7..c3a2dab1a 100644 --- a/src/publicapi.js +++ b/src/publicapi.js @@ -167,7 +167,7 @@ function getInterface(v) { }; _.empty = function() { var root = this.__controller.root, cursor = this.__controller.cursor; - root.eachChild('postOrder', 'dispose'); + root.ends[L] = root.ends[R] = 0; root.jQ.empty(); delete cursor.selection; From df8df90f8f93a2d8c4f7418c5c96c57005c1e401 Mon Sep 17 00:00:00 2001 From: Jason Altekruse Date: Mon, 15 Nov 2021 20:13:47 -0600 Subject: [PATCH 390/393] Fix text block creation --- src/commands/text.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/commands/text.js b/src/commands/text.js index 20aa46370..fdaaf5da0 100644 --- a/src/commands/text.js +++ b/src/commands/text.js @@ -37,11 +37,6 @@ var TextBlock = P(Node, function(_, super_) { 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(); }); - - cursor.insAtRightEnd(textBlock); - - // TODO needs tests - if (textBlock.replacedText) textBlock.write(cursor, textBlock.replacedText); }; _.parser = function() { From 9ac10905d0d131f4559f427ee2b69378d57ca143 Mon Sep 17 00:00:00 2001 From: Jason Merrill Date: Mon, 22 Nov 2021 15:29:53 -0500 Subject: [PATCH 391/393] Fix edit order dependence of supsub height Fixes https://github.com/mathquill/mathquill/issues/950 by reverting https://github.com/mathquill/mathquill/pull/764 and https://github.com/mathquill/mathquill/pull/779 Unfortunately, this leaves us back in a place where superscript and subscript placement isn't very good when they're attached to tall blocks, but I think if we're going to fix that, we need to find a way to do it that's more robust to continuing to edit the expression. --- src/commands/math/commands.js | 22 ---------------------- 1 file changed, 22 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index 445329d17..3a5214ac8 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -363,28 +363,6 @@ var SupSub = P(MathCommand, function(_, super_) { }; }(this, 'sub sup'.split(' ')[i], 'sup sub'.split(' ')[i], 'down up'.split(' ')[i])); }; - _.reflow = function() { - var $block = this.jQ ;//mq-supsub - var $prev = $block.prev() ; - - if ( !$prev.length ) { - //we cant normalize it without having prev. element (which is base) - return ; - } - - var $sup = $block.children( '.mq-sup' );//mq-supsub -> mq-sup - if ( $sup.length ) { - var sup_fontsize = parseInt( $sup.css('font-size') ) ; - var sup_bottom = $sup.offset().top + $sup.height() ; - //we want that superscript overlaps top of base on 0.7 of its font-size - //this way small superscripts like x^2 look ok, but big ones like x^(1/2/3) too - var needed = sup_bottom - $prev.offset().top - 0.7*sup_fontsize ; - var cur_margin = parseInt( $sup.css('margin-bottom' ) ) ; - //we lift it up with margin-bottom - $sup.css( 'margin-bottom', cur_margin + needed ) ; - } - } ; - }); function insLeftOfMeUnlessAtEnd(cursor) { From 21646f282e143acacbc27a785f0825e2eded3228 Mon Sep 17 00:00:00 2001 From: Jason Merrill Date: Mon, 22 Nov 2021 16:03:11 -0500 Subject: [PATCH 392/393] Remove unreferenced air-demo --- test/air-demo.html | 58 ---------------------------------------------- 1 file changed, 58 deletions(-) delete mode 100644 test/air-demo.html diff --git a/test/air-demo.html b/test/air-demo.html deleted file mode 100644 index 76eafdaa2..000000000 --- a/test/air-demo.html +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - MathQuill-basic Demo - - - - - - - -
- - Fork me on GitHub! - -

MathQuill-basic Demo local test page

- -

Backslash \ and dollar $ aren't special, / still works, font works: - -

\sqrt{2} - -

- -

- - - - - - From 29f6be0e070218b26a96c85cad7520d6f363a73e Mon Sep 17 00:00:00 2001 From: Jason Merrill Date: Mon, 22 Nov 2021 16:57:46 -0500 Subject: [PATCH 393/393] Allow running CI with either CircleCI or Jenkins --- test/unit.html | 273 ++++++++++++++++++++++++++++--------------------- 1 file changed, 156 insertions(+), 117 deletions(-) diff --git a/test/unit.html b/test/unit.html index 0d09eec66..ec8ba4946 100644 --- a/test/unit.html +++ b/test/unit.html @@ -28,7 +28,7 @@ mocha.setup({ ui: 'tdd', - reporter: 'html', + reporter: post_xunit_to ? 'xunit' : 'html', bail: false }); @@ -56,138 +56,177 @@

Unit Tests