Merge pull request #591 from italoc-84/patch-1

Add \nexists symbol
diff --git a/README.md b/README.md
index 7b93213..956bee5 100644
--- a/README.md
+++ b/README.md
@@ -144,6 +144,7 @@
 Additionally, descendants of `MQ.EditableField` (currently only `MQ.MathField`)
 expose:
 
+* `.focus()`, `.blur()` focuses or defocuses the editable field
 * `.write(' - 1')` will write some LaTeX at the current cursor position
 * `.cmd('\\sqrt')` will enter a LaTeX command at the current cursor position or
   with the current selection
diff --git a/src/commands/math/advancedSymbols.js b/src/commands/math/advancedSymbols.js
index 085da1c..f5aa5a5 100644
--- a/src/commands/math/advancedSymbols.js
+++ b/src/commands/math/advancedSymbols.js
@@ -222,10 +222,6 @@
 LatexCmds.rbrack = bind(VanillaSymbol, ']');
 
 //various symbols
-LatexCmds['∫'] =
-LatexCmds['int'] =
-LatexCmds.integral = bind(Symbol,'\\int ','<big>&int;</big>');
-
 LatexCmds.slash = bind(VanillaSymbol, '/');
 LatexCmds.vert = bind(VanillaSymbol,'|');
 LatexCmds.perp = LatexCmds.perpendicular = bind(VanillaSymbol,'\\perp ','&perp;');
diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js
index 37c00bf..414edaa 100644
--- a/src/commands/math/commands.js
+++ b/src/commands/math/commands.js
@@ -380,6 +380,10 @@
 LatexCmds.coprod =
 LatexCmds.coproduct = bind(SummationNotation,'\\coprod ','&#8720;');
 
+LatexCmds['∫'] =
+LatexCmds['int'] =
+LatexCmds.integral = bind(Symbol,'\\int ','<big>&int;</big>');
+
 var Fraction =
 LatexCmds.frac =
 LatexCmds.dfrac =
diff --git a/src/commands/text.js b/src/commands/text.js
index 42f85d4..d4352bd 100644
--- a/src/commands/text.js
+++ b/src/commands/text.js
@@ -48,10 +48,8 @@
     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 Fragment();
+
         TextPiece(text).adopt(textBlock, 0, 0);
         return textBlock;
       })
@@ -64,7 +62,11 @@
     });
   };
   _.text = function() { return '"' + this.textContents() + '"'; };
-  _.latex = function() { return '\\text{' + this.textContents() + '}'; };
+  _.latex = function() {
+    var contents = this.textContents();
+    if (contents.length === 0) return '';
+    return '\\text{' + contents + '}';
+  };
   _.html = function() {
     return (
         '<span class="mq-text-mode" mathquill-command-id='+this.id+'>'
@@ -107,7 +109,7 @@
     else { // split apart
       var leftBlock = TextBlock();
       var leftPc = this.ends[L];
-      leftPc.disown();
+      leftPc.disown().jQ.detach();
       leftPc.adopt(leftBlock, 0, 0);
 
       cursor.insLeftOf(this);
@@ -162,15 +164,22 @@
     }
   };
 
-  _.blur = function() {
+  _.blur = function(cursor) {
     MathBlock.prototype.blur.call(this);
-    fuseChildren(this);
+    if (!cursor) return;
+    if (this.textContents() === '') {
+      this.remove();
+      if (cursor[L] === this) cursor[L] = this[L];
+      else if (cursor[R] === this) cursor[R] = this[R];
+    }
+    else fuseChildren(this);
   };
 
   function fuseChildren(self) {
     self.jQ[0].normalize();
 
     var textPcDom = self.jQ[0].firstChild;
+    if (!textPcDom) return;
     pray('only node in TextBlock span is Text node', textPcDom.nodeType === 3);
     // nodeType === 3 has meant a Text node since ancient times:
     //   http://reference.sitepoint.com/javascript/Node/nodeType
diff --git a/src/css/math.less b/src/css/math.less
index 6115ee8..fcaa23d 100644
--- a/src/css/math.less
+++ b/src/css/math.less
@@ -49,7 +49,14 @@
 
 
   .mq-text-mode {
-    font-size: 87%;
+    display: inline-block;
+  }
+  .mq-text-mode.mq-hasCursor {
+    box-shadow: inset darkgray 0 .1em .2em;
+    padding: 0 .1em;
+    margin: 0 -.1em;
+
+    min-width: 1ex;
   }
 
   .mq-font {
diff --git a/src/cursor.js b/src/cursor.js
index b08b2f6..622a023 100644
--- a/src/cursor.js
+++ b/src/cursor.js
@@ -56,7 +56,8 @@
     this[-dir] = oppDir;
     // 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();
+    // FIXME pass cursor to .blur() so text can fix cursor pointers when removing itself
+    if (oldParent !== parent && oldParent.blur) oldParent.blur(this);
   };
   _.insDirOf = function(dir, el) {
     prayDirection(dir);
diff --git a/src/services/latex.js b/src/services/latex.js
index 025f1a8..75369e8 100644
--- a/src/services/latex.js
+++ b/src/services/latex.js
@@ -1,6 +1,6 @@
-// Parser MathCommand
+// Parser MathBlock
 var latexMathParser = (function() {
-  function commandToBlock(cmd) {
+  function commandToBlock(cmd) { // can also take in a Fragment
     var block = MathBlock();
     cmd.adopt(block, 0, 0);
     return block;
diff --git a/test/unit/backspace.test.js b/test/unit/backspace.test.js
index 51a34ca..688d1ba 100644
--- a/test/unit/backspace.test.js
+++ b/test/unit/backspace.test.js
@@ -197,7 +197,7 @@
     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,18 @@
     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(), '', 'the x has been deleted');
+
+    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 7cfa094..4cf59fa 100644
--- a/test/unit/latex.test.js
+++ b/test/unit/latex.test.js
@@ -100,6 +100,7 @@
     assertParsesLatex('\\text { lol! } ', '\\text{ lol! }');
     assertParsesLatex('\\text{apples} \\ne \\text{oranges}',
                       '\\text{apples}\\ne \\text{oranges}');
+    assertParsesLatex('\\text{}', '');
   });
 
   test('not real LaTex commands, but valid symbols', function() {
diff --git a/test/unit/text.test.js b/test/unit/text.test.js
index 6f2ecd3..982a5ac 100644
--- a/test/unit/text.test.js
+++ b/test/unit/text.test.js
@@ -65,4 +65,43 @@
 
     assert.equal(block.latex(), '\\text{x}');
   });
+
+  test('stepping out of an empty block deletes it', function() {
+    var mq = MathQuill.MathField($('<span></span>').appendTo('#mock')[0]);
+    var controller = mq.__controller;
+    var cursor = controller.cursor;
+
+    try {
+      mq.latex('\\text{x}');
+
+      mq.keystroke('Left');
+      assertSplit(cursor.jQ, 'x');
+
+      mq.keystroke('Backspace');
+      assertSplit(cursor.jQ);
+
+      mq.keystroke('Right');
+      assertSplit(cursor.jQ);
+      assert.equal(cursor[L], 0);
+    } finally {
+      $(mq.el()).remove();
+    }
+  });
+
+  test('typing $ in a textblock splits it', function() {
+    var mq = MathQuill.MathField($('<span></span>').appendTo('#mock')[0]);
+    var controller = mq.__controller;
+    var cursor = controller.cursor;
+
+    try {
+      mq.latex('\\text{asdf}');
+      mq.keystroke('Left Left Left');
+      assertSplit(cursor.jQ, 'as', 'df');
+
+      mq.typedText('$');
+      assert.equal(mq.latex(), '\\text{as}\\text{df}');
+    } finally {
+      $(mq.el()).remove();
+    }
+  });
 });