From 1cc1a7fe8cae307b72f4a6f41cce70ff2e07e601 Mon Sep 17 00:00:00 2001 From: eoghanmcilwaine Date: Wed, 24 Jun 2015 19:57:03 -0500 Subject: [PATCH 001/791] Limit textBlock.fuseChildren to text nodes only Prevents it from trying to fuse the cursor span, causing an error. --- src/commands/text.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/text.js b/src/commands/text.js index 47a465e56..b4ebdaee0 100644 --- a/src/commands/text.js +++ b/src/commands/text.js @@ -170,7 +170,7 @@ var TextBlock = P(Node, function(_, super_) { function fuseChildren(self) { self.jQ[0].normalize(); - var textPcDom = self.jQ[0].firstChild; + var textPcDom = self.jQ.contents().filter(function (i, el) { return el.nodeType === 3; })[0] var textPc = TextPiece(textPcDom.data); textPc.jQadd(textPcDom); From 88ed2d70ebbe7b809c7e000b9dac97618533b95e Mon Sep 17 00:00:00 2001 From: Frank Wennerdahl Date: Mon, 28 Sep 2015 22:10:45 +0200 Subject: [PATCH 002/791] Handling of empty text blocks - TextBlock.fuseChildren will now handle the case where no TextPiece is present in the block by inserting a TextPiece containing an empty string. - Added specific implementation of isEmpty to TextBlock to allow empty TextBlocks to be deleted. The Node implementation does not work for TextBlocks; they always contain at least one child so their ends are never 0. - Added test case for parsing \text{} and extended backspacing test for text blocks. --- src/commands/text.js | 8 +++++++- test/unit/backspace.test.js | 20 ++++++++++++++++++-- test/unit/latex.test.js | 1 + 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/src/commands/text.js b/src/commands/text.js index b4ebdaee0..0a596e294 100644 --- a/src/commands/text.js +++ b/src/commands/text.js @@ -84,6 +84,11 @@ var TextBlock = P(Node, function(_, super_) { _.selectTowards = MathCommand.prototype.selectTowards; _.deleteTowards = MathCommand.prototype.deleteTowards; + _.isEmpty = function() { + return (this.ends[L] === 0 || this.ends[L].text === "") && + (this.ends[R] === 0 || this.ends[R].text === ""); + }; + _.selectOutOf = function(dir, cursor) { cursor.insDirOf(dir, this); }; @@ -170,7 +175,8 @@ var TextBlock = P(Node, function(_, super_) { function fuseChildren(self) { self.jQ[0].normalize(); - var textPcDom = self.jQ.contents().filter(function (i, el) { return el.nodeType === 3; })[0] + var textNodes = self.jQ.contents().filter(function (i, el) { return el.nodeType === window.Node.TEXT_NODE; }); + var textPcDom = textNodes.length > 0 ? textNodes[0] : document.createTextNode(""); var textPc = TextPiece(textPcDom.data); textPc.jQadd(textPcDom); diff --git a/test/unit/backspace.test.js b/test/unit/backspace.test.js index ff797889a..a52076688 100644 --- a/test/unit/backspace.test.js +++ b/test/unit/backspace.test.js @@ -197,7 +197,7 @@ suite('backspace', function() { assert.equal(mq.latex(),'n=1'); }); - test('backspace into text block', function() { + test('backspace through text block', function() { mq.latex('\\text{x}'); mq.keystroke('Backspace'); @@ -205,7 +205,23 @@ suite('backspace', function() { var textBlock = rootBlock.ends[R]; assert.equal(cursor.parent, textBlock, 'cursor is in text block'); assert.equal(cursor[R], 0, 'cursor is at the end of text block'); - assert.equal(cursor[L].ctrlSeq, 'x', 'cursor is rightward of the x'); + assert.equal(cursor[L].text, 'x', 'cursor is rightward of the x'); + + mq.keystroke('Backspace'); + assert.equal(cursor.parent, textBlock, 'cursor is still in text block'); + assert.equal(cursor[R], 0, 'cursor is at right the end of text block'); + assert.equal(cursor[L], 0, 'cursor is at left the end of text block'); + assert.equal(mq.latex(), '\\text{}', 'the x has been deleted'); + + mq.keystroke('Backspace'); + assert.equal(cursor.parent, rootBlock, 'cursor is in root block'); + assert.equal(cursor[L], textBlock, 'cursor is to the right of the text block'); + assert.equal(mq.latex(), '\\text{}', 'text block still empty'); + + mq.keystroke('Backspace'); + assert.equal(cursor[R], 0, 'cursor is at the right end of the root block'); + assert.equal(cursor[L], 0, 'cursor is at the left end of the root block'); + assert.equal(mq.latex(), ''); }); suite('empties', function() { diff --git a/test/unit/latex.test.js b/test/unit/latex.test.js index 84a796f2f..a67327797 100644 --- a/test/unit/latex.test.js +++ b/test/unit/latex.test.js @@ -100,6 +100,7 @@ suite('latex', function() { assertParsesLatex('\\text { lol! } ', '\\text{ lol! }'); assertParsesLatex('\\text{apples} \\ne \\text{oranges}', '\\text{apples}\\ne \\text{oranges}'); + assertParsesLatex('\\text{}'); }); suite('public API', function() { From e03362ec78901c85052bdcb8f8901f489a2b97b1 Mon Sep 17 00:00:00 2001 From: eoghanmcilwaine Date: Wed, 24 Jun 2015 18:09:23 -0500 Subject: [PATCH 003/791] Add failing test for textblock/cursor issue After the cursor traverses a TextBlock, the latex output changes to \text{undefined}. --- test/unit/text.test.js | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/unit/text.test.js b/test/unit/text.test.js index 24c0d20be..3cec66f7d 100644 --- a/test/unit/text.test.js +++ b/test/unit/text.test.js @@ -53,4 +53,16 @@ suite('text', function() { ctrlr.moveRight(); assertSplit(cursor.jQ, 'abc', null); }); + + test('does not change latex as the cursor moves around', function() { + var block = fromLatex('\\text{x}'); + var ctrlr = Controller({ __options: 0 }, block); + var cursor = ctrlr.cursor.insAtRightEnd(block); + + ctrlr.moveLeft(); + ctrlr.moveLeft(); + ctrlr.moveLeft(); + + assert.equal(block.latex(), '\\text{x}'); + }); }); From 4a83973969284ef476fe09de32e4ba4e374fde6b Mon Sep 17 00:00:00 2001 From: eoghanmcilwaine Date: Wed, 24 Jun 2015 19:57:03 -0500 Subject: [PATCH 004/791] Limit textBlock.fuseChildren to text nodes only Prevents it from trying to fuse the cursor span, causing an error. --- src/commands/text.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/text.js b/src/commands/text.js index 47a465e56..b4ebdaee0 100644 --- a/src/commands/text.js +++ b/src/commands/text.js @@ -170,7 +170,7 @@ var TextBlock = P(Node, function(_, super_) { function fuseChildren(self) { self.jQ[0].normalize(); - var textPcDom = self.jQ[0].firstChild; + var textPcDom = self.jQ.contents().filter(function (i, el) { return el.nodeType === 3; })[0] var textPc = TextPiece(textPcDom.data); textPc.jQadd(textPcDom); From e2f68932b64b16c9b02ea8351d63c3360d857b53 Mon Sep 17 00:00:00 2001 From: Frank Wennerdahl Date: Mon, 28 Sep 2015 22:10:45 +0200 Subject: [PATCH 005/791] Handling of empty text blocks - TextBlock.fuseChildren will now handle the case where no TextPiece is present in the block by inserting a TextPiece containing an empty string. - Added specific implementation of isEmpty to TextBlock to allow empty TextBlocks to be deleted. The Node implementation does not work for TextBlocks; they always contain at least one child so their ends are never 0. - Added test case for parsing \text{} and extended backspacing test for text blocks. --- src/commands/text.js | 8 +++++++- test/unit/backspace.test.js | 18 +++++++++++++++++- test/unit/latex.test.js | 1 + 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/commands/text.js b/src/commands/text.js index b4ebdaee0..0a596e294 100644 --- a/src/commands/text.js +++ b/src/commands/text.js @@ -84,6 +84,11 @@ var TextBlock = P(Node, function(_, super_) { _.selectTowards = MathCommand.prototype.selectTowards; _.deleteTowards = MathCommand.prototype.deleteTowards; + _.isEmpty = function() { + return (this.ends[L] === 0 || this.ends[L].text === "") && + (this.ends[R] === 0 || this.ends[R].text === ""); + }; + _.selectOutOf = function(dir, cursor) { cursor.insDirOf(dir, this); }; @@ -170,7 +175,8 @@ var TextBlock = P(Node, function(_, super_) { function fuseChildren(self) { self.jQ[0].normalize(); - var textPcDom = self.jQ.contents().filter(function (i, el) { return el.nodeType === 3; })[0] + var textNodes = self.jQ.contents().filter(function (i, el) { return el.nodeType === window.Node.TEXT_NODE; }); + var textPcDom = textNodes.length > 0 ? textNodes[0] : document.createTextNode(""); var textPc = TextPiece(textPcDom.data); textPc.jQadd(textPcDom); diff --git a/test/unit/backspace.test.js b/test/unit/backspace.test.js index 3f566c520..a52076688 100644 --- a/test/unit/backspace.test.js +++ b/test/unit/backspace.test.js @@ -197,7 +197,7 @@ suite('backspace', function() { assert.equal(mq.latex(),'n=1'); }); - test('backspace into text block', function() { + test('backspace through text block', function() { mq.latex('\\text{x}'); mq.keystroke('Backspace'); @@ -206,6 +206,22 @@ suite('backspace', function() { assert.equal(cursor.parent, textBlock, 'cursor is in text block'); assert.equal(cursor[R], 0, 'cursor is at the end of text block'); assert.equal(cursor[L].text, 'x', 'cursor is rightward of the x'); + + mq.keystroke('Backspace'); + assert.equal(cursor.parent, textBlock, 'cursor is still in text block'); + assert.equal(cursor[R], 0, 'cursor is at right the end of text block'); + assert.equal(cursor[L], 0, 'cursor is at left the end of text block'); + assert.equal(mq.latex(), '\\text{}', 'the x has been deleted'); + + mq.keystroke('Backspace'); + assert.equal(cursor.parent, rootBlock, 'cursor is in root block'); + assert.equal(cursor[L], textBlock, 'cursor is to the right of the text block'); + assert.equal(mq.latex(), '\\text{}', 'text block still empty'); + + mq.keystroke('Backspace'); + assert.equal(cursor[R], 0, 'cursor is at the right end of the root block'); + assert.equal(cursor[L], 0, 'cursor is at the left end of the root block'); + assert.equal(mq.latex(), ''); }); suite('empties', function() { diff --git a/test/unit/latex.test.js b/test/unit/latex.test.js index 84a796f2f..a67327797 100644 --- a/test/unit/latex.test.js +++ b/test/unit/latex.test.js @@ -100,6 +100,7 @@ suite('latex', function() { assertParsesLatex('\\text { lol! } ', '\\text{ lol! }'); assertParsesLatex('\\text{apples} \\ne \\text{oranges}', '\\text{apples}\\ne \\text{oranges}'); + assertParsesLatex('\\text{}'); }); suite('public API', function() { From e43356380da4357321c50f886fb2f5a09824781e Mon Sep 17 00:00:00 2001 From: Frank Wennerdahl Date: Tue, 29 Sep 2015 11:49:40 +0200 Subject: [PATCH 006/791] Added failing test to demonstrate issue with stepping in and out of empty text blocks --- test/unit/text.test.js | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/test/unit/text.test.js b/test/unit/text.test.js index 3cec66f7d..07b5765e1 100644 --- a/test/unit/text.test.js +++ b/test/unit/text.test.js @@ -65,4 +65,26 @@ suite('text', function() { assert.equal(block.latex(), '\\text{x}'); }); + + test('updates in the DOM from typing after stepping out of and back into an empty block', function() { + var mq = MathQuill.MathField($('').appendTo('#mock')[0]); + var controller = mq.__controller; + var cursor = controller.cursor; + + mq.latex('\\text{x}'); + + mq.keystroke('Left'); + assertSplit(cursor.jQ, 'x'); + + mq.keystroke('Backspace'); + assertSplit(cursor.jQ); + + mq.keystroke('Right Left'); + assertSplit(cursor.jQ); + + mq.typedText('y'); + assertSplit(cursor.jQ, 'y'); + + $(mq.el()).remove(); + }); }); From 38bc7f16b6ca86e2960504a248eac9359b508f9e Mon Sep 17 00:00:00 2001 From: Frank Wennerdahl Date: Tue, 29 Sep 2015 12:10:34 +0200 Subject: [PATCH 007/791] Fixed teardown for step-in/out test --- test/unit/text.test.js | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/test/unit/text.test.js b/test/unit/text.test.js index 07b5765e1..bb201ea83 100644 --- a/test/unit/text.test.js +++ b/test/unit/text.test.js @@ -71,20 +71,22 @@ suite('text', function() { var controller = mq.__controller; var cursor = controller.cursor; - mq.latex('\\text{x}'); + try { + mq.latex('\\text{x}'); - mq.keystroke('Left'); - assertSplit(cursor.jQ, 'x'); + mq.keystroke('Left'); + assertSplit(cursor.jQ, 'x'); - mq.keystroke('Backspace'); - assertSplit(cursor.jQ); + mq.keystroke('Backspace'); + assertSplit(cursor.jQ); - mq.keystroke('Right Left'); - assertSplit(cursor.jQ); + mq.keystroke('Right Left'); + assertSplit(cursor.jQ); - mq.typedText('y'); - assertSplit(cursor.jQ, 'y'); - - $(mq.el()).remove(); + mq.typedText('y'); + assertSplit(cursor.jQ, 'y'); + } finally { + $(mq.el()).remove(); + } }); }); From 9dd15d4bc9fbbfaf9f3c5373cab0fe44ea3e15a8 Mon Sep 17 00:00:00 2001 From: Frank Wennerdahl Date: Thu, 1 Oct 2015 14:50:28 +0200 Subject: [PATCH 008/791] Fixed test message typos --- test/unit/backspace.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/unit/backspace.test.js b/test/unit/backspace.test.js index a52076688..39d455923 100644 --- a/test/unit/backspace.test.js +++ b/test/unit/backspace.test.js @@ -209,8 +209,8 @@ suite('backspace', function() { mq.keystroke('Backspace'); assert.equal(cursor.parent, textBlock, 'cursor is still in text block'); - assert.equal(cursor[R], 0, 'cursor is at right the end of text block'); - assert.equal(cursor[L], 0, 'cursor is at left the end of text block'); + assert.equal(cursor[R], 0, 'cursor is at the right end of the text block'); + assert.equal(cursor[L], 0, 'cursor is at the left end of the text block'); assert.equal(mq.latex(), '\\text{}', 'the x has been deleted'); mq.keystroke('Backspace'); From 6731f62be6f0a988a90361d8587d8facf25a94c8 Mon Sep 17 00:00:00 2001 From: Frank Wennerdahl Date: Wed, 7 Oct 2015 14:10:32 +0200 Subject: [PATCH 009/791] Fix stepping out and back into empty text blocks Reverted the insertion of empty text pieces and handled text blocks containing no text pieces instead. --- src/commands/text.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/commands/text.js b/src/commands/text.js index 0a596e294..f3972b39f 100644 --- a/src/commands/text.js +++ b/src/commands/text.js @@ -84,11 +84,6 @@ var TextBlock = P(Node, function(_, super_) { _.selectTowards = MathCommand.prototype.selectTowards; _.deleteTowards = MathCommand.prototype.deleteTowards; - _.isEmpty = function() { - return (this.ends[L] === 0 || this.ends[L].text === "") && - (this.ends[R] === 0 || this.ends[R].text === ""); - }; - _.selectOutOf = function(dir, cursor) { cursor.insDirOf(dir, this); }; @@ -127,7 +122,7 @@ var TextBlock = P(Node, function(_, super_) { // insert cursor at approx position in DOMTextNode var avgChWidth = this.jQ.width()/this.text.length; var approxPosition = Math.round((pageX - this.jQ.offset().left)/avgChWidth); - if (approxPosition <= 0) cursor.insAtLeftEnd(this); + if (approxPosition <= 0 || textPc === null) cursor.insAtLeftEnd(this); else if (approxPosition >= textPc.text.length) cursor.insAtRightEnd(this); else cursor.insLeftOf(textPc.splitRight(approxPosition)); @@ -176,12 +171,16 @@ var TextBlock = P(Node, function(_, super_) { self.jQ[0].normalize(); var textNodes = self.jQ.contents().filter(function (i, el) { return el.nodeType === window.Node.TEXT_NODE; }); - var textPcDom = textNodes.length > 0 ? textNodes[0] : document.createTextNode(""); - var textPc = TextPiece(textPcDom.data); - textPc.jQadd(textPcDom); + var textPc = null; + + if (textNodes.length > 0) { + var textPcDom = textNodes[0]; + textPc = TextPiece(textPcDom.data); + textPc.jQadd(textPcDom); + } self.children().disown(); - return textPc.adopt(self, 0, 0); + return textPc !== null ? textPc.adopt(self, 0, 0) : null; } _.focus = MathBlock.prototype.focus; From d9e8884110cb25c8f5205f6061ad49191b7982d8 Mon Sep 17 00:00:00 2001 From: Frank Wennerdahl Date: Thu, 8 Oct 2015 12:01:25 +0200 Subject: [PATCH 010/791] Deleting out of text blocks moves cursor out --- src/commands/text.js | 1 + test/unit/text.test.js | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/commands/text.js b/src/commands/text.js index f3972b39f..ec173099a 100644 --- a/src/commands/text.js +++ b/src/commands/text.js @@ -90,6 +90,7 @@ var TextBlock = P(Node, function(_, super_) { _.deleteOutOf = function(dir, cursor) { // backspace and delete at ends of block don't unwrap if (this.isEmpty()) cursor.insRightOf(this); + else cursor.insDirOf(dir, this); }; _.write = function(cursor, ch) { cursor.show().deleteSelection(); diff --git a/test/unit/text.test.js b/test/unit/text.test.js index bb201ea83..88ebdffcd 100644 --- a/test/unit/text.test.js +++ b/test/unit/text.test.js @@ -89,4 +89,45 @@ suite('text', function() { $(mq.el()).remove(); } }); + + suite('deleteOutOf', function() { + var mq, cursor, rootBlock, textBlock; + + setup(function() { + mq = MathQuill.MathField($('').appendTo('#mock')[0]); + cursor = mq.__controller.cursor; + rootBlock = mq.__controller.root; + mq.latex("\\text{x}"); + textBlock = rootBlock.ends[L]; + }); + teardown(function() { + $(mq.el()).remove(); + }); + + test('moves cursor to the left when backspacing out', function() { + mq.keystroke("Left Left"); + assert.equal(cursor.parent, textBlock, 'cursor is in text block'); + assert.equal(cursor[R].text, 'x', 'cursor to the left of the text'); + assert.equal(cursor[L], 0, 'cursor at left end of the text block'); + + mq.keystroke('Backspace'); + assert.equal(cursor.parent, rootBlock, 'cursor is in root block'); + assert.equal(cursor[R], textBlock, 'cursor to the left of the text block'); + assert.equal(cursor[L], 0, 'cursor at left end of the root block'); + assert.equal(mq.latex(), '\\text{x}', 'text block still present'); + }); + + test('moves cursor to the right when deleting out', function() { + mq.keystroke("Left"); + assert.equal(cursor.parent, textBlock, 'cursor is in text block'); + assert.equal(cursor[R], 0, 'cursor at right end of the text block'); + assert.equal(cursor[L].text, 'x', 'cursor to the right of the text'); + + mq.keystroke('Del'); + assert.equal(cursor.parent, rootBlock, 'cursor is in root block'); + assert.equal(cursor[R], 0, 'cursor at right end of the root block'); + assert.equal(cursor[L], textBlock, 'cursor to the right of the text block'); + assert.equal(mq.latex(), '\\text{x}', 'text block still present'); + }); + }); }); From 571911698f1cd581744cb7c0a853e519d9302b0a Mon Sep 17 00:00:00 2001 From: Frank Wennerdahl Date: Mon, 19 Oct 2015 11:30:29 +0200 Subject: [PATCH 011/791] Revert "Deleting out of text blocks moves cursor out" This reverts commit d9e8884110cb25c8f5205f6061ad49191b7982d8. --- src/commands/text.js | 1 - test/unit/text.test.js | 41 ----------------------------------------- 2 files changed, 42 deletions(-) diff --git a/src/commands/text.js b/src/commands/text.js index ec173099a..f3972b39f 100644 --- a/src/commands/text.js +++ b/src/commands/text.js @@ -90,7 +90,6 @@ var TextBlock = P(Node, function(_, super_) { _.deleteOutOf = function(dir, cursor) { // backspace and delete at ends of block don't unwrap if (this.isEmpty()) cursor.insRightOf(this); - else cursor.insDirOf(dir, this); }; _.write = function(cursor, ch) { cursor.show().deleteSelection(); diff --git a/test/unit/text.test.js b/test/unit/text.test.js index 88ebdffcd..bb201ea83 100644 --- a/test/unit/text.test.js +++ b/test/unit/text.test.js @@ -89,45 +89,4 @@ suite('text', function() { $(mq.el()).remove(); } }); - - suite('deleteOutOf', function() { - var mq, cursor, rootBlock, textBlock; - - setup(function() { - mq = MathQuill.MathField($('').appendTo('#mock')[0]); - cursor = mq.__controller.cursor; - rootBlock = mq.__controller.root; - mq.latex("\\text{x}"); - textBlock = rootBlock.ends[L]; - }); - teardown(function() { - $(mq.el()).remove(); - }); - - test('moves cursor to the left when backspacing out', function() { - mq.keystroke("Left Left"); - assert.equal(cursor.parent, textBlock, 'cursor is in text block'); - assert.equal(cursor[R].text, 'x', 'cursor to the left of the text'); - assert.equal(cursor[L], 0, 'cursor at left end of the text block'); - - mq.keystroke('Backspace'); - assert.equal(cursor.parent, rootBlock, 'cursor is in root block'); - assert.equal(cursor[R], textBlock, 'cursor to the left of the text block'); - assert.equal(cursor[L], 0, 'cursor at left end of the root block'); - assert.equal(mq.latex(), '\\text{x}', 'text block still present'); - }); - - test('moves cursor to the right when deleting out', function() { - mq.keystroke("Left"); - assert.equal(cursor.parent, textBlock, 'cursor is in text block'); - assert.equal(cursor[R], 0, 'cursor at right end of the text block'); - assert.equal(cursor[L].text, 'x', 'cursor to the right of the text'); - - mq.keystroke('Del'); - assert.equal(cursor.parent, rootBlock, 'cursor is in root block'); - assert.equal(cursor[R], 0, 'cursor at right end of the root block'); - assert.equal(cursor[L], textBlock, 'cursor to the right of the text block'); - assert.equal(mq.latex(), '\\text{x}', 'text block still present'); - }); - }); }); From fa1c07312ac3be616e02d3a35e30616702879010 Mon Sep 17 00:00:00 2001 From: Frank Wennerdahl Date: Mon, 19 Oct 2015 12:06:17 +0200 Subject: [PATCH 012/791] Empty text blocks are ignored when parsing from and exporting to latex --- src/commands/text.js | 14 +++++++++----- src/services/latex.js | 2 +- test/unit/backspace.test.js | 5 +++-- test/unit/latex.test.js | 2 +- 4 files changed, 14 insertions(+), 9 deletions(-) diff --git a/src/commands/text.js b/src/commands/text.js index f3972b39f..27831e64a 100644 --- a/src/commands/text.js +++ b/src/commands/text.js @@ -48,10 +48,8 @@ var TextBlock = P(Node, function(_, super_) { return optWhitespace .then(string('{')).then(regex(/^[^}]*/)).skip(string('}')) .map(function(text) { - // TODO: is this the correct behavior when parsing - // the latex \text{} ? This violates the requirement that - // the text contents are always nonempty. Should we just - // disown the parent node instead? + if (text.length === 0) return; + TextPiece(text).adopt(textBlock, 0, 0); return textBlock; }) @@ -64,7 +62,13 @@ var TextBlock = P(Node, function(_, super_) { }); }; _.text = function() { return '"' + this.textContents() + '"'; }; - _.latex = function() { return '\\text{' + this.textContents() + '}'; }; + _.latex = function() { + var contents = this.textContents(); + + if (contents.length === 0) return ''; + + return '\\text{' + this.textContents() + '}'; + }; _.html = function() { return ( '' diff --git a/src/services/latex.js b/src/services/latex.js index c5489511e..c5a10616b 100644 --- a/src/services/latex.js +++ b/src/services/latex.js @@ -2,7 +2,7 @@ var latexMathParser = (function() { function commandToBlock(cmd) { var block = MathBlock(); - cmd.adopt(block, 0, 0); + if (cmd) cmd.adopt(block, 0, 0); return block; } function joinBlocks(blocks) { diff --git a/test/unit/backspace.test.js b/test/unit/backspace.test.js index 39d455923..07db90573 100644 --- a/test/unit/backspace.test.js +++ b/test/unit/backspace.test.js @@ -206,17 +206,18 @@ suite('backspace', function() { assert.equal(cursor.parent, textBlock, 'cursor is in text block'); assert.equal(cursor[R], 0, 'cursor is at the end of text block'); assert.equal(cursor[L].text, 'x', 'cursor is rightward of the x'); + assert.equal(mq.latex(), '\\text{x}', 'the x has been deleted'); mq.keystroke('Backspace'); assert.equal(cursor.parent, textBlock, 'cursor is still in text block'); assert.equal(cursor[R], 0, 'cursor is at the right end of the text block'); assert.equal(cursor[L], 0, 'cursor is at the left end of the text block'); - assert.equal(mq.latex(), '\\text{}', 'the x has been deleted'); + assert.equal(mq.latex(), '', 'the x has been deleted'); mq.keystroke('Backspace'); assert.equal(cursor.parent, rootBlock, 'cursor is in root block'); assert.equal(cursor[L], textBlock, 'cursor is to the right of the text block'); - assert.equal(mq.latex(), '\\text{}', 'text block still empty'); + assert.equal(mq.latex(), '', 'text block still empty'); mq.keystroke('Backspace'); assert.equal(cursor[R], 0, 'cursor is at the right end of the root block'); diff --git a/test/unit/latex.test.js b/test/unit/latex.test.js index a67327797..a152cd5e5 100644 --- a/test/unit/latex.test.js +++ b/test/unit/latex.test.js @@ -100,7 +100,7 @@ suite('latex', function() { assertParsesLatex('\\text { lol! } ', '\\text{ lol! }'); assertParsesLatex('\\text{apples} \\ne \\text{oranges}', '\\text{apples}\\ne \\text{oranges}'); - assertParsesLatex('\\text{}'); + assertParsesLatex('\\text{}', ''); }); suite('public API', function() { From c02a3d700ed7c47d54a4a9f082ac3eecae637261 Mon Sep 17 00:00:00 2001 From: Frank Wennerdahl Date: Mon, 16 Nov 2015 14:49:16 +0100 Subject: [PATCH 013/791] Added base class for diacritics above letters - \vec now uses the DiacriticAbove base class. - Added \tilde command that also uses the same base class. --- src/commands/math/commands.js | 30 +++++++++++++++++++----------- src/css/math.less | 4 ++-- 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index be5a2698f..db23c76d5 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -457,17 +457,6 @@ LatexCmds['√'] = P(MathCommand, function(_, super_) { }; }); -var Vec = LatexCmds.vec = P(MathCommand, function(_, super_) { - _.ctrlSeq = '\\vec'; - _.htmlTemplate = - '' - + '' - + '&0' - + '' - ; - _.textTemplate = ['vec(', ')']; -}); - var NthRoot = LatexCmds.nthroot = P(SquareRoot, function(_, super_) { _.htmlTemplate = @@ -483,6 +472,25 @@ LatexCmds.nthroot = P(SquareRoot, function(_, super_) { }; }); +var DiacriticAbove = P(MathCommand, function(_, super_) { + _.init = function(ctrlSeq, symbol, textTemplate) { + var htmlTemplate = + '' + + ''+symbol+'' + + '&0' + + '' + ; + + super_.init.call(this, ctrlSeq, htmlTemplate, textTemplate); + }; +}); + +var Vec = +LatexCmds.vec = bind(DiacriticAbove, '\\vec', '→', ['vec(', ')']); + +var Tilde = +LatexCmds.tilde = bind(DiacriticAbove, '\\tilde', '~', ['tilde(', ')']); + function DelimsMixin(_, super_) { _.jQadd = function() { super_.jQadd.apply(this, arguments); diff --git a/src/css/math.less b/src/css/math.less index 954d27c79..a627fca46 100644 --- a/src/css/math.less +++ b/src/css/math.less @@ -271,7 +271,7 @@ padding-top: 1px; } - .mq-vector-prefix { + .mq-diacritic-above { display: block; text-align: center; line-height: .25em; @@ -279,7 +279,7 @@ font-size: 0.75em; } - .mq-vector-stem { + .mq-diacritic-stem { display: block; } From 8f90fd1dc433a2bd8b9471b4bceb77e74f51bba1 Mon Sep 17 00:00:00 2001 From: Frank Wennerdahl Date: Mon, 16 Nov 2015 15:20:04 +0100 Subject: [PATCH 014/791] Proposed CSS changes for diacritical marks --- src/css/math.less | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/css/math.less b/src/css/math.less index a627fca46..b8f6dff57 100644 --- a/src/css/math.less +++ b/src/css/math.less @@ -274,13 +274,12 @@ .mq-diacritic-above { display: block; text-align: center; - line-height: .25em; - margin-bottom: -.1em; - font-size: 0.75em; + line-height: .4em; } .mq-diacritic-stem { display: block; + text-align: center; } .mq-large-operator { From 81520d812d5badf01be2f0bd36ec18b18080e054 Mon Sep 17 00:00:00 2001 From: Stephen Clower Date: Wed, 24 Feb 2016 15:48:30 -0500 Subject: [PATCH 015/791] 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 016/791] 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 017/791] 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 e36ab571b6dff1430c626c9eda73ab7255e4a95e Mon Sep 17 00:00:00 2001 From: Han Seoul-Oh Date: Fri, 26 Feb 2016 17:35:54 -0800 Subject: [PATCH 018/791] Add test for \tilde; simplify its HTML No need for a character entity reference, it's not a special character in HTML (it's only special in LaTeX). --- src/commands/math/commands.js | 2 +- test/visual.html | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index db23c76d5..c5194b4f2 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -489,7 +489,7 @@ var Vec = LatexCmds.vec = bind(DiacriticAbove, '\\vec', '→', ['vec(', ')']); var Tilde = -LatexCmds.tilde = bind(DiacriticAbove, '\\tilde', '~', ['tilde(', ')']); +LatexCmds.tilde = bind(DiacriticAbove, '\\tilde', '~', ['tilde(', ')']); function DelimsMixin(_, super_) { _.jQadd = function() { diff --git a/test/visual.html b/test/visual.html index 73345f900..570bf7e52 100644 --- a/test/visual.html +++ b/test/visual.html @@ -198,6 +198,7 @@

Static LaTeX rendering (.mathquill-static-math) tests

1+\sum_0^n+\sum_{i=0123}^n+\sum_0^{wordiness}1+\sum_0^n+\sum_{i=0123}^n+\sum_0^{wordiness}^M x\ \ \ +\ \ \ yx\ \ \ +\ \ \ y^M \sum _{n=0}^3\cos x\sum _{n=0}^3\cos x^M +\vec x + \tilde x + \vec A + \tilde A + \vec{abcd} + \tilde{abcd}\vec x + \tilde x + \vec A + \tilde A + \vec{abcd} + \tilde{abcd}^M

Parentheses vertical alignment at font sizes ranging from 10px to 24px:

From b5ba5424fd2fb9dc122e79a8cd330d9bc52ec9fe Mon Sep 17 00:00:00 2001 From: Stephen Clower Date: Mon, 29 Feb 2016 14:13:34 -0500 Subject: [PATCH 019/791] 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 020/791] 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 021/791] 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 022/791] 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 023/791] 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 024/791] 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 025/791] 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 026/791] 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 027/791] 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 028/791] 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 029/791] 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 2a431d284d2e6cc9cb9e05ef30c66949503466ac Mon Sep 17 00:00:00 2001 From: David Lippman Date: Fri, 4 Mar 2016 00:05:28 -0800 Subject: [PATCH 030/791] Add hat command --- src/commands/math/commands.js | 11 +++++++++++ src/css/math.less | 12 ++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index bfc2f1a14..4c030ca2a 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -468,6 +468,17 @@ var Vec = LatexCmds.vec = P(MathCommand, function(_, super_) { _.textTemplate = ['vec(', ')']; }); +var Hat = LatexCmds.hat = P(MathCommand, function(_, super_) { + _.ctrlSeq = '\\hat'; + _.htmlTemplate = + '' + + '^' + + '&0' + + '' + ; + _.textTemplate = ['hat(', ')']; +}); + var NthRoot = LatexCmds.nthroot = P(SquareRoot, function(_, super_) { _.htmlTemplate = diff --git a/src/css/math.less b/src/css/math.less index 954d27c79..295364379 100644 --- a/src/css/math.less +++ b/src/css/math.less @@ -282,6 +282,18 @@ .mq-vector-stem { display: block; } + + .mq-hat-prefix { + display: block; + text-align: center; + line-height: .95em; + margin-bottom: -.7em; + transform:scale(1.5,1); + } + + .mq-hat-stem { + display: block; + } .mq-large-operator { text-align: center; From 1f6282cb5c4083ea0370c88a6b97c86773937056 Mon Sep 17 00:00:00 2001 From: Han Seoul-Oh Date: Fri, 4 Mar 2016 00:11:09 -0800 Subject: [PATCH 031/791] 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 032/791] 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 033/791] 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 034/791] 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 035/791] 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 036/791] 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 $(' - + + - + diff --git a/test/unit/focusBlur.test.js b/test/unit/focusBlur.test.js index 20b4fbda5..1b30e9f6d 100644 --- a/test/unit/focusBlur.test.js +++ b/test/unit/focusBlur.test.js @@ -22,7 +22,7 @@ suite('focusBlur', function() { }); function triggerUpOutOf(mq) { - $(mq.el()).find('textarea').trigger(jQuery.Event('keydown', { which: 38 })); + $(mq.el()).find('textarea').trigger(jQuery.extend(jQuery.Event('keydown'), { which: 38 })); assert.ok(wasUpOutOfCalled); } @@ -83,4 +83,21 @@ suite('focusBlur', function() { }, 100); }); }); + + test('blur event fired when math field loses focus', function(done) { + var mq = MQ.MathField($('').appendTo('#mock')[0]); + + mq.focus(); + assertHasFocus(mq, 'math field'); + + var textarea = $(' + + + + + + + + From 91a917d892363b0c17d6f42474a24267a5169354 Mon Sep 17 00:00:00 2001 From: skondam Date: Fri, 23 Feb 2018 18:45:37 -0600 Subject: [PATCH 625/791] 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 626/791] 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 627/791] 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 628/791] 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 629/791] 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 630/791] 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 631/791] 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 632/791] 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 36e20d60cdb78093160768ae4324f015649e28cc Mon Sep 17 00:00:00 2001 From: Pankaj Phartiyal Date: Thu, 29 Mar 2018 00:27:09 +0530 Subject: [PATCH 633/791] #797 less 3.0.1 breaks build because @var:; is non standard https://github.com/less/less.js/issues/2911 --- src/css/font.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/css/font.less b/src/css/font.less index 6c7b1dc8a..234b16201 100644 --- a/src/css/font.less +++ b/src/css/font.less @@ -1,4 +1,4 @@ -@omit-font-face:; +@omit-font-face:~""; .font-face; .font-face() when not (@omit-font-face) { @font-face { @@ -7,7 +7,7 @@ } } -@basic:; +@basic:~""; .font-srcs() when not (@basic) { src: url(fonts/Symbola.eot); src: From ac78a347da194a615488b25ab137989d8e30a0d3 Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Fri, 18 May 2018 11:14:34 -0400 Subject: [PATCH 634/791] 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 635/791] 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 636/791] 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 637/791] 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 638/791] 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 639/791] 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 640/791] 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 641/791] 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 642/791] 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 643/791] 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 644/791] 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 645/791] 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 646/791] 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 647/791] 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 648/791] 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 649/791] 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 650/791] 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 651/791] 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 652/791] 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 653/791] 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 654/791] 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 655/791] 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 656/791] 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 657/791] 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 658/791] 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 659/791] 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 660/791] 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 661/791] 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 662/791] 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 663/791] 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 664/791] 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 7f7f0af6f7f1a4591bce8b257a539bc12a5a1dda Mon Sep 17 00:00:00 2001 From: Han Seoul-Oh Date: Fri, 8 Feb 2019 13:19:40 -0800 Subject: [PATCH 665/791] Change MathQuill docs theme back to RTD from MkDocs For reasons I don't understand, the docs site http://docs.mathquill.com/ had its theme changed from the default ReadTheDocs theme to the MkDocs built-in default theme. This should switch it back: https://mkdocs.readthedocs.io/en/stable/#theming-our-documentation --- mkdocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/mkdocs.yml b/mkdocs.yml index 5351f6ee8..870730bae 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -18,5 +18,6 @@ markdown_extensions: - toc: permalink: True +theme: readthedocs extra_css: - extra.css From 1bc06ba7310eae902ba5ee7461cf53413ec5699d Mon Sep 17 00:00:00 2001 From: Steve Clower Date: Wed, 13 Feb 2019 12:15:55 -0500 Subject: [PATCH 666/791] 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 667/791] 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 668/791] 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 669/791] 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 670/791] 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 671/791] 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 672/791] 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 673/791] 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 674/791] 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 675/791] 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 676/791] 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 677/791] 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 678/791] 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 679/791] 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 680/791] 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 681/791] 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 682/791] 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 683/791] 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 684/791] 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 685/791] 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 686/791] 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 687/791] 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 688/791] 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 689/791] 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 690/791] 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 691/791] 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 692/791] 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 693/791] 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 694/791] 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 695/791] 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 696/791] 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 697/791] 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 698/791] 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 699/791] 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 700/791] 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 701/791] 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 702/791] 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 703/791] 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 704/791] 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 705/791] 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 706/791] 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 707/791] 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 708/791] 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 709/791] 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 710/791] 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 711/791] 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 4d773adca009881c61316bf7007a68505f090b50 Mon Sep 17 00:00:00 2001 From: sashaalesin Date: Fri, 8 May 2020 03:48:36 +0300 Subject: [PATCH 712/791] Add parentheses to superscript text template --- 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 afce8c98d..c92b6d055 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -346,7 +346,7 @@ LatexCmds['^'] = P(SupSub, function(_, super_) { + '&0' + '' ; - _.textTemplate = [ '^' ]; + _.textTemplate = ['^(', ')']; _.finalizeTree = function() { this.upInto = this.sup = this.ends[R]; this.sup.downOutOf = insLeftOfMeUnlessAtEnd; From b19a9dd518e9cf40c84004d30a4020a1f513f381 Mon Sep 17 00:00:00 2001 From: sashaalesin Date: Fri, 8 May 2020 04:45:29 +0300 Subject: [PATCH 713/791] Add text method to SupSub --- src/commands/math/commands.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js index c92b6d055..1889ba6a9 100644 --- a/src/commands/math/commands.js +++ b/src/commands/math/commands.js @@ -250,6 +250,13 @@ var SupSub = P(MathCommand, function(_, super_) { } return latex('_', this.sub) + latex('^', this.sup); }; + _.text = function() { + function text(prefix, block) { + var l = block && block.text(); + return block ? prefix + (l.length === 1 ? l : '(' + (l || ' ') + ')') : ''; + } + return text('_', this.sub) + text('^', this.sup); + }; _.addBlock = function(block) { if (this.supsub === 'sub') { this.sup = this.upInto = this.sub.upOutOf = block; From 7fef30675bff771879fff0ac6a446ae96f1cf510 Mon Sep 17 00:00:00 2001 From: Eli Luberoff Date: Mon, 18 May 2020 16:51:33 -0700 Subject: [PATCH 714/791] 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 96bf1389b8934fd4fc6b53afc88a9eef5f65f24b Mon Sep 17 00:00:00 2001 From: Han Seoul-Oh Date: Mon, 18 May 2020 19:51:15 -0700 Subject: [PATCH 715/791] Fix #794: TextBlocks dispatch reflow at the wrong time Also fix #705, supersede and close #847 Reflow should obviously dispatch after the contents of the TextBlock is populated, commit log doesn't show any reason it was placed before, it's been that way since text was refactored into TextBlock: https://github.com/mathquill/mathquill/commit/c4ba68398fc3e9e5cbb4b00c63430ea12efa6727#diff-d7064a7fc8599c06242c4c9ba3264fdbR28 Similarly after writing a character in TextBlock, should clearly dispatch a reflow, but it hasn't from the beginning: https://github.com/mathquill/mathquill/commit/c4ba68398fc3e9e5cbb4b00c63430ea12efa6727#diff-d7064a7fc8599c06242c4c9ba3264fdbR98 Also: - update demo.html to use edit handlers, since we tell API clients to - update typing and text test suites to check LaTeX-during-edit-event, piggybacking on current LaTeX checks Recommend -w to view diff. --- src/commands/text.js | 11 ++++--- test/demo.html | 24 ++++++++------ test/unit/text.test.js | 70 +++++++++++++++++++++++++++------------- test/unit/typing.test.js | 17 ++++++++-- 4 files changed, 84 insertions(+), 38 deletions(-) diff --git a/src/commands/text.js b/src/commands/text.js index 5af29462b..2da06d9a3 100644 --- a/src/commands/text.js +++ b/src/commands/text.js @@ -27,15 +27,15 @@ var TextBlock = P(Node, function(_, super_) { var textBlock = this; super_.createLeftOf.call(this, cursor); - if (textBlock[R].siblingCreated) textBlock[R].siblingCreated(cursor.options, L); - if (textBlock[L].siblingCreated) textBlock[L].siblingCreated(cursor.options, R); - textBlock.bubble('reflow'); - cursor.insAtRightEnd(textBlock); if (textBlock.replacedText) for (var i = 0; i < textBlock.replacedText.length; i += 1) textBlock.write(cursor, textBlock.replacedText.charAt(i)); + + if (textBlock[R].siblingCreated) textBlock[R].siblingCreated(cursor.options, L); + if (textBlock[L].siblingCreated) textBlock[L].siblingCreated(cursor.options, R); + textBlock.bubble('reflow'); }; _.parser = function() { @@ -113,8 +113,9 @@ var TextBlock = P(Node, function(_, super_) { leftPc.adopt(leftBlock, 0, 0); cursor.insLeftOf(this); - super_.createLeftOf.call(leftBlock, cursor); + super_.createLeftOf.call(leftBlock, cursor); // micro-optimization, not for correctness } + this.bubble('reflow'); }; _.seek = function(pageX, cursor) { diff --git a/test/demo.html b/test/demo.html index 2eb473b42..d54bf8527 100644 --- a/test/demo.html +++ b/test/demo.html @@ -94,17 +94,20 @@

MathQuill Demo local test p $(function() { var latexMath = MQ($('#editable-math')[0]); - $(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())); - htmlTransplantExample.html(latexMath.html()); - }); - }).keydown(); + latexMath.config({ + handlers: { + edit: function() { + var latex = latexMath.latex(); + latexSource.val(latex); +// location.hash = '#'+latex; //extremely performance-crippling in Chrome for some reason + htmlSource.text(printTree(latexMath.html())); + htmlTransplantExample.html(latexMath.html()); + } + } + }); latexMath.focus(); + latexSource.val(latexMath.latex()); latexSource.bind('keydown keypress', function() { var oldtext = latexSource.val(); setTimeout(function() { @@ -117,6 +120,9 @@

MathQuill Demo local test p }); }); + htmlSource.text(printTree(latexMath.html())); + htmlTransplantExample.html(latexMath.html()); + if(location.hash && location.hash.length > 1) latexMath.latex(decodeURIComponent(location.hash.slice(1))).focus(); }); diff --git a/test/unit/text.test.js b/test/unit/text.test.js index 11b2617e7..51b87aae0 100644 --- a/test/unit/text.test.js +++ b/test/unit/text.test.js @@ -66,34 +66,60 @@ suite('text', function() { assert.equal(block.latex(), '\\text{x}'); }); - test('stepping out of an empty block deletes it', function() { - var mq = MathQuill.MathField($('').appendTo('#mock')[0]); - var controller = mq.__controller; - var cursor = controller.cursor; + suite('typing', function() { + var mq, mostRecentlyReportedLatex; + setup(function() { + mostRecentlyReportedLatex = NaN; // != to everything + mq = MQ.MathField($('').appendTo('#mock')[0], { + handlers: { + edit: function() { + mostRecentlyReportedLatex = mq.latex(); + } + } + }); + }); + + function prayWellFormedPoint(pt) { prayWellFormed(pt.parent, pt[L], pt[R]); } + function assertLatex(latex) { + prayWellFormedPoint(mq.__controller.cursor); + assert.equal(mostRecentlyReportedLatex, latex); + assert.equal(mq.latex(), latex); + } - mq.latex('\\text{x}'); + test('stepping out of an empty block deletes it', function() { + var controller = mq.__controller; + var cursor = controller.cursor; - mq.keystroke('Left'); - assertSplit(cursor.jQ, 'x'); + mq.latex('\\text{x}'); + assertLatex('\\text{x}'); - mq.keystroke('Backspace'); - assertSplit(cursor.jQ); + mq.keystroke('Left'); + assertSplit(cursor.jQ, 'x'); + assertLatex('\\text{x}'); - mq.keystroke('Right'); - assertSplit(cursor.jQ); - assert.equal(cursor[L], 0); - }); + mq.keystroke('Backspace'); + assertSplit(cursor.jQ); + assertLatex(''); + + mq.keystroke('Right'); + assertSplit(cursor.jQ); + assert.equal(cursor[L], 0); + assertLatex(''); + }); + + test('typing $ in a textblock splits it', function() { + var controller = mq.__controller; + var cursor = controller.cursor; - test('typing $ in a textblock splits it', function() { - var mq = MathQuill.MathField($('').appendTo('#mock')[0]); - var controller = mq.__controller; - var cursor = controller.cursor; + mq.latex('\\text{asdf}'); + assertLatex('\\text{asdf}'); - mq.latex('\\text{asdf}'); - mq.keystroke('Left Left Left'); - assertSplit(cursor.jQ, 'as', 'df'); + mq.keystroke('Left Left Left'); + assertSplit(cursor.jQ, 'as', 'df'); + assertLatex('\\text{asdf}'); - mq.typedText('$'); - assert.equal(mq.latex(), '\\text{as}\\text{df}'); + mq.typedText('$'); + assertLatex('\\text{as}\\text{df}'); + }); }); }); diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js index c77fc9084..f64724fc3 100644 --- a/test/unit/typing.test.js +++ b/test/unit/typing.test.js @@ -1,12 +1,20 @@ suite('typing with auto-replaces', function() { - var mq; + var mq, mostRecentlyReportedLatex; setup(function() { - mq = MQ.MathField($('').appendTo('#mock')[0]); + mostRecentlyReportedLatex = NaN; // != to everything + mq = MQ.MathField($('').appendTo('#mock')[0], { + handlers: { + edit: function() { + mostRecentlyReportedLatex = mq.latex(); + } + } + }); }); function prayWellFormedPoint(pt) { prayWellFormed(pt.parent, pt[L], pt[R]); } function assertLatex(latex) { prayWellFormedPoint(mq.__controller.cursor); + assert.equal(mostRecentlyReportedLatex, latex); assert.equal(mq.latex(), latex); } @@ -49,6 +57,11 @@ suite('typing with auto-replaces', function() { }); test('nonexistent LaTeX command', function() { + mq.typedText('\\asdf').keystroke('Enter'); + assertLatex('\\text{asdf}'); + }); + + test('nonexistent LaTeX command, then symbol', function() { mq.typedText('\\asdf+'); assertLatex('\\text{asdf}+'); }); From 19a8db4e695fc7f3ddc5cea91fe6698cf408b696 Mon Sep 17 00:00:00 2001 From: Han Seoul-Oh Date: Mon, 18 May 2020 20:21:48 -0700 Subject: [PATCH 716/791] Update circle.yml from 1.0 to 2.0 using config-translation https://circleci.com/docs/2.0/config-translation/ Apparently 1.0 was EOLed on 2018-08-31 :/ --- circle.yml | 464 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 266 insertions(+), 198 deletions(-) diff --git a/circle.yml b/circle.yml index 7fd97e52a..20df15488 100644 --- a/circle.yml +++ b/circle.yml @@ -30,219 +30,287 @@ # this file is based on https://github.com/circleci/sauce-connect/blob/a65e41c91e02550ce56c75740a422bebc4acbf6f/circle.yml # via https://circleci.com/docs/browser-testing-with-sauce-labs/ +# +# then translated from 1.0 to 2.0 with: https://circleci.com/docs/2.0/config-translation/ + +version: 2 +jobs: + build: + working_directory: ~/mathquill/mathquill + parallelism: 1 + shell: /bin/bash --login + # CircleCI 2.0 does not support environment variables that refer to each other the same way as 1.0 did. + # If any of these refer to each other, rewrite them so that they don't or see https://circleci.com/docs/2.0/env-vars/#interpolating-environment-variables-to-set-other-environment-variables . + environment: + CIRCLE_ARTIFACTS: /tmp/circleci-artifacts + CIRCLE_TEST_REPORTS: /tmp/circleci-test-results + # In CircleCI 1.0 we used a pre-configured image with a large number of languages and other packages. + # In CircleCI 2.0 you can now specify your own image, or use one of our pre-configured images. + # The following configuration line tells CircleCI to use the specified docker image as the runtime environment for you job. + # We have selected a pre-built image that mirrors the build environment we use on + # the 1.0 platform, but we recommend you choose an image more tailored to the needs + # of each job. For more information on choosing an image (or alternatively using a + # VM instead of a container) see https://circleci.com/docs/2.0/executor-types/ + # To see the list of pre-built images that CircleCI provides for most common languages see + # https://circleci.com/docs/2.0/circleci-images/ + docker: + - image: circleci/build-image:ubuntu-14.04-XXL-upstart-1189-5614f37 + command: /sbin/init + steps: + # Machine Setup + # If you break your build into multiple jobs with workflows, you will probably want to do the parts of this that are relevant in each + # The following `checkout` command checks out your code to your working directory. In 1.0 we did this implicitly. In 2.0 you can choose where in the course of a job your code should be checked out. + - checkout + # Prepare for artifact and test results collection equivalent to how it was done on 1.0. + # In many cases you can simplify this from what is generated here. + # 'See docs on artifact collection here https://circleci.com/docs/2.0/artifacts/' + - run: mkdir -p $CIRCLE_ARTIFACTS $CIRCLE_TEST_REPORTS + # Dependencies + # This would typically go in either a build or a build-and-test job when using workflows + # Restore the dependency cache + - restore_cache: + keys: + # This branch if available + - v1-dep-{{ .Branch }}- + # Default branch if not + - v1-dep-master- + # Any branch if there are none on the default branch - this should be unnecessary if you have your default branch configured correctly + - v1-dep- + # This is based on your 1.0 configuration file or project settings + - run: + command: |- + # SauceConnect: download if not cached, and launch with retry + test $SAUCE_USERNAME && test $SAUCE_ACCESS_KEY || { + echo 'Sauce Labs credentials required. Sign up here: https://saucelabs.com/opensauce/' + exit 1 + } + + mkdir -p ~/sauce-connect + cd ~/sauce-connect + + if [ -x sc-*-linux/bin/sc ]; then + echo Using cached sc-*-linux/bin/sc + else + time wget https://saucelabs.com/downloads/sc-latest-linux.tar.gz + time tar -xzf sc-latest-linux.tar.gz + fi -dependencies: - cache_directories: - - ~/sauce-connect - pre: - - ? |- - # SauceConnect: download if not cached, and launch with retry - test $SAUCE_USERNAME && test $SAUCE_ACCESS_KEY || { - echo 'Sauce Labs credentials required. Sign up here: https://saucelabs.com/opensauce/' - exit 1 - } - - mkdir -p ~/sauce-connect - cd ~/sauce-connect + time sc-*-linux/bin/sc --user $SAUCE_USERNAME --api-key $SAUCE_ACCESS_KEY \ + --readyfile ~/sauce_is_ready + test -e ~/sauce_was_ready && exit - if [ -x sc-*-linux/bin/sc ]; then - echo Using cached sc-*-linux/bin/sc - else + echo 'Sauce Connect failed, try redownloading (https://git.io/vSxsJ)' + rm -rf * time wget https://saucelabs.com/downloads/sc-latest-linux.tar.gz time tar -xzf sc-latest-linux.tar.gz - fi - - time sc-*-linux/bin/sc --user $SAUCE_USERNAME --api-key $SAUCE_ACCESS_KEY \ - --readyfile ~/sauce_is_ready - test -e ~/sauce_was_ready && exit - echo 'Sauce Connect failed, try redownloading (https://git.io/vSxsJ)' - rm -rf * - time wget https://saucelabs.com/downloads/sc-latest-linux.tar.gz - time tar -xzf sc-latest-linux.tar.gz + time sc-*-linux/bin/sc --user $SAUCE_USERNAME --api-key $SAUCE_ACCESS_KEY \ + --readyfile ~/sauce_is_ready + test -e ~/sauce_was_ready && exit - time sc-*-linux/bin/sc --user $SAUCE_USERNAME --api-key $SAUCE_ACCESS_KEY \ - --readyfile ~/sauce_is_ready - test -e ~/sauce_was_ready && exit - - echo 'ERROR: Exited twice without creating readyfile' \ - | tee /dev/stderr > ~/sauce_is_ready - exit 1 - : + echo 'ERROR: Exited twice without creating readyfile' \ + | tee /dev/stderr > ~/sauce_is_ready + exit 1 background: true - -test: - pre: - - |- - # Generate link to Many-Worlds build and add to GitHub Commit Status - curl -i -X POST https://api.github.com/repos/mathquill/mathquill/statuses/$CIRCLE_SHA1 \ - -u MathQuillBot:$GITHUB_STATUS_API_KEY \ - -d '{ - "context": "ci/many-worlds", - "state": "success", - "description": "Try the tests on the Many-Worlds build of this commit:", - "target_url": "http://many-worlds.glitch.me/mathquill/mathquill/commit/'$CIRCLE_SHA1'/test/" - }' - + # The following line was run implicitly in your 1.0 builds based on what CircleCI inferred about the structure of your project. In 2.0 you need to be explicit about which commands should be run. In some cases you can discard inferred commands if they are not relevant to your project. + - run: if [ -z "${NODE_ENV:-}" ]; then export NODE_ENV=test; fi + - run: export PATH="~/mathquill/mathquill/node_modules/.bin:$PATH" + - run: npm install + # Save dependency cache + - save_cache: + key: v1-dep-{{ .Branch }}-{{ epoch }} + paths: + # This is a broad list of cache paths to include many possible development environments + # You can probably delete some of these entries + - vendor/bundle + - ~/virtualenvs + - ~/.m2 + - ~/.ivy2 + - ~/.bundle + - ~/.go_workspace + - ~/.gradle + - ~/.cache/bower + # These cache paths were specified in the 1.0 config + - ~/sauce-connect + - ./node_modules + # Test + # This would typically be a build job when using workflows, possibly combined with build + # This is based on your 1.0 configuration file or project settings + - run: |- + # Generate link to Many-Worlds build and add to GitHub Commit Status + curl -i -X POST https://api.github.com/repos/mathquill/mathquill/statuses/$CIRCLE_SHA1 \ + -u MathQuillBot:$GITHUB_STATUS_API_KEY \ + -d '{ + "context": "ci/many-worlds", + "state": "success", + "description": "Try the tests on the Many-Worlds build of this commit:", + "target_url": "http://many-worlds.glitch.me/mathquill/mathquill/commit/'$CIRCLE_SHA1'/test/" + }' # Safari on Sauce can only connect to port 3000, 4000, 7000, or 8000. Edge needs port 7000 or 8000. # https://david263a.wordpress.com/2015/04/18/fixing-safari-cant-connect-to-localhost-issue-when-using-sauce-labs-connect-tunnel/ # https://support.saucelabs.com/customer/portal/questions/14368823-requests-to-localhost-on-microsoft-edge-are-failing-over-sauce-connect - - PORT=8000 make server: + - run: + command: PORT=8000 make server background: true - # Wait for tunnel to be ready (`make server` is much faster, no need to wait for it) - - while [ ! -e ~/sauce_is_ready ]; do sleep 1; done; touch ~/sauce_was_ready; test -z "$(<~/sauce_is_ready)" - - override: - - ? |- - # Screenshots: capture in the background while running unit tests - mkdir -p $CIRCLE_TEST_REPORTS/mocha - - # CircleCI expects test results to be reported in an JUnit/xUnit-style XML file: - # https://circleci.com/docs/test-metadata/#a-namemochajsamocha-for-nodejs - # Our unit tests are in a browser, so they can't write to a file, and Sauce - # apparently truncates custom data in their test result reports, so instead we - # POST to this trivial Node server on localhost:9000 that writes the body of - # any POST request to $CIRCLE_TEST_REPORTS/junit/test-results.xml - node -e ' - require("http").createServer(function(req, res) { - res.setHeader("Access-Control-Allow-Origin", "*"); - req.pipe(process.stdout); - req.on("end", res.end.bind(res)); - }) - .listen(9000); - console.error("listening on http://0.0.0.0:9000/"); - ' 2>&1 >$CIRCLE_TEST_REPORTS/junit/test-results.xml | { - # ^ note: `2>&1` must precede `>$CIRCLE_TEST_REPORTS/...` because - # shell redirect is like assignment; if it came after, then both - # stdout and stderr would be written to `xunit.xml` and nothing - # would be piped into here - - head -1 # wait for "listening on ..." to be logged - - # https://circleci.com/docs/environment-variables/ - build_name="CircleCI build #$CIRCLE_BUILD_NUM" - if [ $CIRCLE_PR_NUMBER ]; then - build_name="$build_name: PR #$CIRCLE_PR_NUMBER" - [ "$CIRCLE_BRANCH" ] && build_name="$build_name ($CIRCLE_BRANCH)" - else - build_name="$build_name: $CIRCLE_BRANCH" - fi - build_name="$build_name @ ${CIRCLE_SHA1:0:7}" - export MQ_CI_BUILD_NAME="$build_name" - - time { test -d node_modules/wd || npm install wd; } - time node script/screenshots.js http://localhost:8000/test/visual.html \ - && touch ~/screenshots_are_ready || echo EXIT STATUS $? | tee /dev/stderr > ~/screenshots_are_ready: - } - : + - run: while [ ! -e ~/sauce_is_ready ]; do sleep 1; done; touch ~/sauce_was_ready; test -z "$(<~/sauce_is_ready)" + # This is based on your 1.0 configuration file or project settings + - run: + command: |- + # Screenshots: capture in the background while running unit tests + mkdir -p $CIRCLE_TEST_REPORTS/mocha + + # CircleCI expects test results to be reported in an JUnit/xUnit-style XML file: + # https://circleci.com/docs/test-metadata/#a-namemochajsamocha-for-nodejs + # Our unit tests are in a browser, so they can't write to a file, and Sauce + # apparently truncates custom data in their test result reports, so instead we + # POST to this trivial Node server on localhost:9000 that writes the body of + # any POST request to $CIRCLE_TEST_REPORTS/junit/test-results.xml + node -e ' + require("http").createServer(function(req, res) { + res.setHeader("Access-Control-Allow-Origin", "*"); + req.pipe(process.stdout); + req.on("end", res.end.bind(res)); + }) + .listen(9000); + console.error("listening on http://0.0.0.0:9000/"); + ' 2>&1 >$CIRCLE_TEST_REPORTS/junit/test-results.xml | { + # ^ note: `2>&1` must precede `>$CIRCLE_TEST_REPORTS/...` because + # shell redirect is like assignment; if it came after, then both + # stdout and stderr would be written to `xunit.xml` and nothing + # would be piped into here + + head -1 # wait for "listening on ..." to be logged + + # https://circleci.com/docs/environment-variables/ + build_name="CircleCI build #$CIRCLE_BUILD_NUM" + if [ $CIRCLE_PR_NUMBER ]; then + build_name="$build_name: PR #$CIRCLE_PR_NUMBER" + [ "$CIRCLE_BRANCH" ] && build_name="$build_name ($CIRCLE_BRANCH)" + else + build_name="$build_name: $CIRCLE_BRANCH" + fi + build_name="$build_name @ ${CIRCLE_SHA1:0:7}" + export MQ_CI_BUILD_NAME="$build_name" + + time { test -d node_modules/wd || npm install wd; } + time node script/screenshots.js http://localhost:8000/test/visual.html \ + && touch ~/screenshots_are_ready || echo EXIT STATUS $? | tee /dev/stderr > ~/screenshots_are_ready: + } background: true + - run: |- + # Unit tests in the browser - - |- - # Unit tests in the browser - - echo '1. Launch tests' - echo - - # https://circleci.com/docs/environment-variables/ - build_name="CircleCI build #$CIRCLE_BUILD_NUM" - if [ $CIRCLE_PR_NUMBER ]; then - build_name="$build_name: PR #$CIRCLE_PR_NUMBER" - [ "$CIRCLE_BRANCH" ] && build_name="$build_name ($CIRCLE_BRANCH)" - else - build_name="$build_name: $CIRCLE_BRANCH" - fi - build_name="$build_name @ ${CIRCLE_SHA1:0:7}" - - # "build" and "customData" parameters from: - # https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-TestAnnotation - set -o pipefail - curl -i -X POST https://saucelabs.com/rest/v1/$SAUCE_USERNAME/js-tests \ - -u $SAUCE_USERNAME:$SAUCE_ACCESS_KEY \ - -H 'Content-Type: application/json' \ - -d '{ - "name": "Unit tests, Mocha", - "build": "'"$build_name"'", - "customData": {"build_url": "'"$CIRCLE_BUILD_URL"'"}, - "framework": "mocha", - "url": "http://localhost:8000/test/unit.html?post_xunit_to=http://localhost:9000", - "platforms": [["", "Chrome", ""]] - }' \ - | tee /dev/stderr | tail -1 > js-tests.json - - echo '2. Wait for tests to finish:' - echo - # > Make the request multiple times as the tests run until the response - # > contains `completed: true` to the get the final results. - # https://wiki.saucelabs.com/display/DOCS/JavaScript+Unit+Testing+Methods - while true # Bash has no do...while >:( - do - sleep 5 - curl -i -X POST https://saucelabs.com/rest/v1/$SAUCE_USERNAME/js-tests/status \ + echo '1. Launch tests' + echo + + # https://circleci.com/docs/environment-variables/ + build_name="CircleCI build #$CIRCLE_BUILD_NUM" + if [ $CIRCLE_PR_NUMBER ]; then + build_name="$build_name: PR #$CIRCLE_PR_NUMBER" + [ "$CIRCLE_BRANCH" ] && build_name="$build_name ($CIRCLE_BRANCH)" + else + build_name="$build_name: $CIRCLE_BRANCH" + fi + build_name="$build_name @ ${CIRCLE_SHA1:0:7}" + + # "build" and "customData" parameters from: + # https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-TestAnnotation + set -o pipefail + curl -i -X POST https://saucelabs.com/rest/v1/$SAUCE_USERNAME/js-tests \ -u $SAUCE_USERNAME:$SAUCE_ACCESS_KEY \ -H 'Content-Type: application/json' \ - -d @js-tests.json \ - | tee /dev/stderr | tail -1 > status.json - - # deliberately do `... != false` rather than `... == true` - # because unexpected values should break rather than infinite loop - [ "$(jq .completed js-tests.json + + echo '2. Wait for tests to finish:' + echo + # > Make the request multiple times as the tests run until the response + # > contains `completed: true` to the get the final results. + # https://wiki.saucelabs.com/display/DOCS/JavaScript+Unit+Testing+Methods + while true # Bash has no do...while >:( + do + sleep 5 + curl -i -X POST https://saucelabs.com/rest/v1/$SAUCE_USERNAME/js-tests/status \ + -u $SAUCE_USERNAME:$SAUCE_ACCESS_KEY \ + -H 'Content-Type: application/json' \ + -d @js-tests.json \ + | tee /dev/stderr | tail -1 > status.json + + # deliberately do `... != false` rather than `... == true` + # because unexpected values should break rather than infinite loop + [ "$(jq .completed Date: Mon, 18 May 2020 20:41:59 -0700 Subject: [PATCH 717/791] Attempt to fix CI This suggests removing /sbin/init: https://discuss.circleci.com/t/circleci-was-unable-to-run-the-job-runner/31894/10 --- circle.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/circle.yml b/circle.yml index 20df15488..1e9d3d872 100644 --- a/circle.yml +++ b/circle.yml @@ -55,7 +55,6 @@ jobs: # https://circleci.com/docs/2.0/circleci-images/ docker: - image: circleci/build-image:ubuntu-14.04-XXL-upstart-1189-5614f37 - command: /sbin/init steps: # Machine Setup # If you break your build into multiple jobs with workflows, you will probably want to do the parts of this that are relevant in each From e7944e23348fa56114d61f70303b5d29e6300cd1 Mon Sep 17 00:00:00 2001 From: Han Seoul-Oh Date: Mon, 18 May 2020 20:53:45 -0700 Subject: [PATCH 718/791] Upgrade CircleCI Docker image Apparently circleci/build-image is no longer being updated: https://discuss.circleci.com/t/circleci-was-unable-to-run-the-job-runner/31894/18 I ... don't understand these DockerHub images but lts-browsers seems reasonable: https://hub.docker.com/r/circleci/node --- circle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/circle.yml b/circle.yml index 1e9d3d872..61e3022b2 100644 --- a/circle.yml +++ b/circle.yml @@ -54,7 +54,7 @@ jobs: # To see the list of pre-built images that CircleCI provides for most common languages see # https://circleci.com/docs/2.0/circleci-images/ docker: - - image: circleci/build-image:ubuntu-14.04-XXL-upstart-1189-5614f37 + - image: circleci/node:lts-browsers steps: # Machine Setup # If you break your build into multiple jobs with workflows, you will probably want to do the parts of this that are relevant in each From e9baa3d42200490044da2d89100147193b2ebbe1 Mon Sep 17 00:00:00 2001 From: Han Seoul-Oh Date: Mon, 18 May 2020 22:47:10 -0700 Subject: [PATCH 719/791] Fix broken Sauce Connect download link sc-latest-linux.tar.gz 404s now, and I don't think it was documented anywhere ever (I got it from the ancient, unmaintained circleci example [1]), so now I'm just using the latest version from the Sauce Wiki, v4.5.4: https://wiki.saucelabs.com/display/DOCS/Downloading+Sauce+Connect+Proxy [1]: https://github.com/circleci/sauce-connect/blob/master/circle.yml --- circle.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/circle.yml b/circle.yml index 61e3022b2..29cdb51e3 100644 --- a/circle.yml +++ b/circle.yml @@ -90,8 +90,8 @@ jobs: if [ -x sc-*-linux/bin/sc ]; then echo Using cached sc-*-linux/bin/sc else - time wget https://saucelabs.com/downloads/sc-latest-linux.tar.gz - time tar -xzf sc-latest-linux.tar.gz + time wget https://saucelabs.com/downloads/sc-4.5.4-linux.tar.gz + time tar -xzf sc-4.5.4-linux.tar.gz fi time sc-*-linux/bin/sc --user $SAUCE_USERNAME --api-key $SAUCE_ACCESS_KEY \ @@ -100,8 +100,8 @@ jobs: echo 'Sauce Connect failed, try redownloading (https://git.io/vSxsJ)' rm -rf * - time wget https://saucelabs.com/downloads/sc-latest-linux.tar.gz - time tar -xzf sc-latest-linux.tar.gz + time wget https://saucelabs.com/downloads/sc-4.5.4-linux.tar.gz + time tar -xzf sc-4.5.4-linux.tar.gz time sc-*-linux/bin/sc --user $SAUCE_USERNAME --api-key $SAUCE_ACCESS_KEY \ --readyfile ~/sauce_is_ready From 8007f9e165f169e63f52d7f548bcbdcd44f65fb3 Mon Sep 17 00:00:00 2001 From: Jason Merrill Date: Tue, 2 Jun 2020 15:52:51 -0700 Subject: [PATCH 720/791] 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 721/791] 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 07460a15683e5037f381c59e78e8d7b783e0f677 Mon Sep 17 00:00:00 2001 From: Jason Altekruse Date: Thu, 10 Sep 2020 20:11:01 -0500 Subject: [PATCH 722/791] Fix pasting into text blocks - previously it would insert a math node into a text block, if you happened to paste in latex - if you pasted in somthing that wasn't latex you would get mathquill source in the latex output - Update test framework to print both the generic message like a failure when comparing two values as well as the cutom message priovided if there is one --- src/commands/math.js | 19 ++++++++ src/commands/text.js | 5 ++ src/services/latex.js | 17 +------ test/support/assert.js | 3 +- test/unit/text.test.js | 106 +++++++++++++++++++++++++++++++++-------- 5 files changed, 111 insertions(+), 39 deletions(-) diff --git a/src/commands/math.js b/src/commands/math.js index acc1cf730..ab66256f3 100644 --- a/src/commands/math.js +++ b/src/commands/math.js @@ -458,6 +458,25 @@ var MathBlock = P(MathElement, function(_, super_) { } }; + _.writeLatex = function(cursor, latex) { + + var all = Parser.all; + var eof = Parser.eof; + + var block = latexMathParser.skip(eof).or(all.result(false)).parse(latex); + + if (block && !block.isEmpty() && block.prepareInsertionAt(cursor)) { + block.children().adopt(cursor.parent, cursor[L], cursor[R]); + var jQ = block.jQize(); + jQ.insertBefore(cursor.jQ); + cursor[L] = block.ends[R]; + block.finalizeInsert(cursor.options, cursor); + if (block.ends[R][R].siblingCreated) block.ends[R][R].siblingCreated(cursor.options, L); + if (block.ends[L][L].siblingCreated) block.ends[L][L].siblingCreated(cursor.options, R); + cursor.parent.bubble('reflow'); + } + }; + _.focus = function() { this.jQ.addClass('mq-hasCursor'); this.jQ.removeClass('mq-empty'); diff --git a/src/commands/text.js b/src/commands/text.js index 2da06d9a3..f1dcc4771 100644 --- a/src/commands/text.js +++ b/src/commands/text.js @@ -117,6 +117,11 @@ var TextBlock = P(Node, function(_, super_) { } this.bubble('reflow'); }; + _.writeLatex = function(cursor, latex) { + if (!cursor[L]) TextPiece(latex).createLeftOf(cursor); + else cursor[L].appendText(latex); + this.bubble('reflow'); + }; _.seek = function(pageX, cursor) { cursor.hide(); diff --git a/src/services/latex.js b/src/services/latex.js index 16b396a7e..a9fd123a1 100644 --- a/src/services/latex.js +++ b/src/services/latex.js @@ -83,22 +83,7 @@ Controller.open(function(_, super_) { }; _.writeLatex = function(latex) { var cursor = this.notify('edit').cursor; - - var all = Parser.all; - var eof = Parser.eof; - - var block = latexMathParser.skip(eof).or(all.result(false)).parse(latex); - - if (block && !block.isEmpty() && block.prepareInsertionAt(cursor)) { - block.children().adopt(cursor.parent, cursor[L], cursor[R]); - var jQ = block.jQize(); - jQ.insertBefore(cursor.jQ); - cursor[L] = block.ends[R]; - block.finalizeInsert(cursor.options, cursor); - if (block.ends[R][R].siblingCreated) block.ends[R][R].siblingCreated(cursor.options, L); - if (block.ends[L][L].siblingCreated) block.ends[L][L].siblingCreated(cursor.options, R); - cursor.parent.bubble('reflow'); - } + cursor.parent.writeLatex(cursor, latex); return this; }; diff --git a/test/support/assert.js b/test/support/assert.js index d659c9b19..0b8ae8a52 100644 --- a/test/support/assert.js +++ b/test/support/assert.js @@ -3,8 +3,7 @@ window.assert = (function() { if (!opts) opts = {}; $.extend(this, opts); - - if (!this.message) this.message = this.explanation; + this.message = this.explanation + ' ' + this.message; Error.call(this, this.message); } diff --git a/test/unit/text.test.js b/test/unit/text.test.js index 51b87aae0..120aeb579 100644 --- a/test/unit/text.test.js +++ b/test/unit/text.test.js @@ -1,5 +1,24 @@ suite('text', function() { + var mq, mostRecentlyReportedLatex; + setup(function() { + mostRecentlyReportedLatex = NaN; // != to everything + mq = MQ.MathField($('').appendTo('#mock')[0], { + handlers: { + edit: function() { + mostRecentlyReportedLatex = mq.latex(); + } + } + }); + }); + + function prayWellFormedPoint(pt) { prayWellFormed(pt.parent, pt[L], pt[R]); } + function assertLatex(latex) { + prayWellFormedPoint(mq.__controller.cursor); + assert.equal(mostRecentlyReportedLatex, latex, 'assertLatex failed'); + assert.equal(mq.latex(), latex, 'assertLatex failed'); + } + function fromLatex(latex) { var block = latexMathParser.parse(latex); block.jQize(); @@ -12,7 +31,7 @@ suite('text', function() { if (prev) { assert.ok(dom.previousSibling instanceof Text); - assert.equal(prev, dom.previousSibling.data); + assert.equal(prev, dom.previousSibling.data, 'assertSplit failed'); } else { assert.ok(!dom.previousSibling); @@ -20,7 +39,7 @@ suite('text', function() { if (next) { assert.ok(dom.nextSibling instanceof Text); - assert.equal(next, dom.nextSibling.data); + assert.equal(next, dom.nextSibling.data, 'assertSplit failed'); } else { assert.ok(!dom.nextSibling); @@ -67,25 +86,6 @@ suite('text', function() { }); suite('typing', function() { - var mq, mostRecentlyReportedLatex; - setup(function() { - mostRecentlyReportedLatex = NaN; // != to everything - mq = MQ.MathField($('').appendTo('#mock')[0], { - handlers: { - edit: function() { - mostRecentlyReportedLatex = mq.latex(); - } - } - }); - }); - - function prayWellFormedPoint(pt) { prayWellFormed(pt.parent, pt[L], pt[R]); } - function assertLatex(latex) { - prayWellFormedPoint(mq.__controller.cursor); - assert.equal(mostRecentlyReportedLatex, latex); - assert.equal(mq.latex(), latex); - } - test('stepping out of an empty block deletes it', function() { var controller = mq.__controller; var cursor = controller.cursor; @@ -122,4 +122,68 @@ suite('text', function() { assertLatex('\\text{as}\\text{df}'); }); }); + + suite('pasting', function() { + test('sanity', function() { + var controller = mq.__controller; + var cursor = controller.cursor; + + mq.latex('\\text{asdf}'); + mq.keystroke('Left Left Left'); + assertSplit(cursor.jQ, 'as', 'df'); + + controller.paste('foo'); + + assertSplit(cursor.jQ, 'asfoo', 'df'); + assertLatex('\\text{asfoodf}'); + prayWellFormedPoint(cursor); + + }); + + test('pasting a dollar sign', function() { + var controller = mq.__controller; + var cursor = controller.cursor; + + mq.latex('\\text{asdf}'); + mq.keystroke('Left Left Left'); + assertSplit(cursor.jQ, 'as', 'df'); + + controller.paste('$foo'); + + assertSplit(cursor.jQ, 'as$foo', 'df'); + assertLatex('\\text{as$foodf}'); + prayWellFormedPoint(cursor); + }); + + test('pasting a backslash', function() { + var controller = mq.__controller; + var cursor = controller.cursor; + + mq.latex('\\text{asdf}'); + mq.keystroke('Left Left Left'); + assertSplit(cursor.jQ, 'as', 'df'); + + controller.paste('\\pi'); + + assertSplit(cursor.jQ, 'as\\pi', 'df'); + assertLatex('\\text{as\\backslash pidf}'); + prayWellFormedPoint(cursor); + }); + + test('pasting a curly brace', function() { + var controller = mq.__controller; + var cursor = controller.cursor; + + mq.latex('\\text{asdf}'); + mq.keystroke('Left Left Left'); + assertSplit(cursor.jQ, 'as', 'df'); + + controller.paste('{'); + + assertSplit(cursor.jQ, 'as{', 'df'); + assertLatex('\\text{as\\{df}'); + prayWellFormedPoint(cursor); + }); + + }); }); From 35891a439a865c099978c4fba5194adfa180fa81 Mon Sep 17 00:00:00 2001 From: Jason Altekruse Date: Thu, 10 Sep 2020 21:13:41 -0500 Subject: [PATCH 723/791] Fix round tripping for mathbb symbols --- src/commands/math/advancedSymbols.js | 20 +++++++++++++++++++- test/unit/latex.test.js | 13 ++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/src/commands/math/advancedSymbols.js b/src/commands/math/advancedSymbols.js index 8609bdf7b..1cd72f042 100644 --- a/src/commands/math/advancedSymbols.js +++ b/src/commands/math/advancedSymbols.js @@ -70,8 +70,26 @@ LatexCmds.nsupersete = LatexCmds.nsuperseteq = LatexCmds.notsupersete = LatexCmds.notsuperseteq = bind(BinaryOperator,'\\not\\supseteq ','⊉'); - //the canonical sets of numbers +LatexCmds.mathbb = P(MathCommand, function(_) { + _.createLeftOf = noop; + _.numBlocks = function() { return 1; }; + _.parser = function() { + var string = Parser.string; + var regex = Parser.regex; + var optWhitespace = Parser.optWhitespace; + return optWhitespace.then(string('{')) + .then(optWhitespace) + .then(regex(/^[NPZQRCH]/)) + .skip(optWhitespace) + .skip(string('}')) + .map(function(c) { + // instantiate the class for the matching char + return LatexCmds[c](); + }); + }; +}); + LatexCmds.N = LatexCmds.naturals = LatexCmds.Naturals = bind(VanillaSymbol,'\\mathbb{N}','ℕ'); diff --git a/test/unit/latex.test.js b/test/unit/latex.test.js index 12d3b4efe..48b29cfe6 100644 --- a/test/unit/latex.test.js +++ b/test/unit/latex.test.js @@ -23,6 +23,17 @@ suite('latex', function() { assertParsesLatex('PNZQRCH'); }); + test('can parse mathbb symbols', function() { + assertParsesLatex('\\P\\N\\Z\\Q\\R\\C\\H', + '\\mathbb{P}\\mathbb{N}\\mathbb{Z}\\mathbb{Q}\\mathbb{R}\\mathbb{C}\\mathbb{H}'); + assertParsesLatex('\\mathbb{P}\\mathbb{N}\\mathbb{Z}\\mathbb{Q}\\mathbb{R}\\mathbb{C}\\mathbb{H}'); + }); + + test('can parse mathbb error case', function() { + assert.throws(function() { assertParsesLatex('\\mathbb + 2'); }); + assert.throws(function() { assertParsesLatex('\\mathbb{A}'); }); + }); + test('simple exponent', function() { assertParsesLatex('x^n'); }); @@ -132,7 +143,7 @@ suite('latex', function() { latexMathParser.parse('\\left\\lVerte123\\right\\rVerte'); }) }); - + test('parens with whitespace', function() { assertParsesLatex('\\left ( 123 \\right ) ', '\\left(123\\right)'); }); From 666ed8e6b6e153992cd2ac6ae13c4de7be9039fa Mon Sep 17 00:00:00 2001 From: Han Seoul-Oh Date: Mon, 14 Sep 2020 18:21:47 -0700 Subject: [PATCH 724/791] Fix unit test page to work on CI Latest Mocha on CI (v8.1.3), Mocha.utils.parseQuery() has apparently been removed. @jaltekruse recommends convenient new URLSearchParams: https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams/get Unit tests are just JS, should always be run in a recent browser (on CI we use latest Chrome), so it's fine to use a modern API. --- test/unit.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/unit.html b/test/unit.html index 936982410..23d82d11f 100644 --- a/test/unit.html +++ b/test/unit.html @@ -24,7 +24,7 @@