Merge pull request #580 from mathquill/build.nodejs-vs-node

Makefile nice err msg if no Node or installed wrong
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3d2d549..4a787ed 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,40 @@
+## v0.10.1: 2016-03-21
+
+Important fix: remove `font-size: 0` on textarea (#585), fixing typing
+in Chrome Canary (#540) as well as the Enter key not triggering the
+`enter` handler in Webkit and Blink (#566). `transform: scale(0)` is
+used instead and expected to be much more robust.
+
+(Note: if you're coming from v0.9.x, there've been major API changes,
+see the [v0.9.x → v0.10.0 Migration Guide][].)
+
+[v0.9.x → v0.10.0 Migration Guide]: https://github.com/mathquill/mathquill/wiki/v0.9.x-%E2%86%92-v0.10.0-Migration-Guide
+
+**new features:**
+- (#544, #552, #558, #581) new symbols `\nparallel`, `\measuredangle`,
+  `\odot`, `\parallelogram` (nonstandard), `\nless`, `\ngtr`, `\square`
+- (#544) new commands `\overleftarrow`, `\overrightarrow`
+
+
+**bugfixes:**
+- (#585) fix typing in Chrome Canary, Enter key in Webkit+Blink
+- (#582) fix `\degree` symbol to round-trip (rather than exporting
+  `^\circ` which doesn't parse as one symbol)
+- (#578) fix `.text()` to output `\cdot` as `*`
+- (#529, #571, #574) fix `.text()` of fractions, spaces, variables followed
+  by exponents
+- (#577) fix `\triangle` symbol to match LaTeX better
+- (#568) hotfix #435 order-dependence breaking clean build on Linux
+- (#560) fix florin spacing still too close
+- (#546) fix parsing or pasting `×` (Unicode times symbol)
+- (#519/#487) fix auto-horizontal-scroll/pan on API calls
+- (#528) fix #429 can't move cursor out of `TextBlock`
+- (#526) fix exponentiation to export `^` not `**`
+- (#525) fix Tab while there's a selection
+
+**build system fixes:**
+- (#532) add console output to show URL of local test pages
+
 ## v0.10.0: 2016-02-20
 
 Many major changes including a total overhaul of the API (no more
diff --git a/Makefile b/Makefile
index 7ce1207..8acc2a6 100644
--- a/Makefile
+++ b/Makefile
@@ -75,8 +75,9 @@
 CLEAN += $(BUILD_DIR)/*
 
 DISTDIR = ./mathquill-$(VERSION)
-DIST = $(DISTDIR).tgz
-CLEAN += $(DIST)
+DISTTAR = $(DISTDIR).tgz
+DISTZIP = $(DISTDIR).zip
+CLEAN += $(DISTTAR) $(DISTZIP)
 
 # programs and flags
 UGLIFY ?= ./node_modules/.bin/uglifyjs
@@ -111,7 +112,6 @@
 uglify: $(UGLY_JS)
 css: $(BUILD_CSS)
 font: $(FONT_TARGET)
-dist: $(DIST)
 clean:
 	rm -rf $(CLEAN)
 
@@ -136,7 +136,7 @@
 	$(LESSC) --modify-var="basic=true" $(LESS_OPTS) $(CSS_MAIN) > $@
 
 $(NODE_MODULES_INSTALLED): package.json
-	npm install
+	NODE_ENV=development npm install
 	touch $(NODE_MODULES_INSTALLED)
 
 $(BUILD_DIR_EXISTS):
@@ -147,10 +147,11 @@
 	rm -rf $@
 	cp -r $< $@
 
-$(DIST): $(UGLY_JS) $(BUILD_JS) $(BUILD_CSS) $(FONT_TARGET)
+dist: $(UGLY_JS) $(BUILD_JS) $(BUILD_CSS) $(FONT_TARGET)
 	rm -rf $(DISTDIR)
 	cp -r $(BUILD_DIR) $(DISTDIR)
-	tar -czf $(DIST) --exclude='\.gitkeep' $(DISTDIR)
+	zip -r -X $(DISTZIP) $(DISTDIR)
+	tar -czf $(DISTTAR) $(DISTDIR)
 	rm -r $(DISTDIR)
 
 #
@@ -166,83 +167,3 @@
 
 $(BUILD_TEST): $(INTRO) $(SOURCES_FULL) $(UNIT_TESTS) $(OUTRO) $(BUILD_DIR_EXISTS)
 	cat $^ > $@
-
-#
-# -*- site (mathquill.github.com) tasks
-#
-
-.PHONY: site publish site-pull
-
-SITE = mathquill.github.com
-SITE_CLONE_URL = git@github.com:mathquill/mathquill.github.com
-SITE_COMMITMSG = 'updating mathquill to $(VERSION)'
-
-DOWNLOADS_PAGE = $(SITE)/downloads.html
-DIST_DOWNLOAD = $(SITE)/downloads/$(DIST)
-
-site: $(SITE) $(SITE)/mathquill $(SITE)/demo.html $(SITE)/support $(DOWNLOADS_PAGE)
-
-publish: site-pull site
-	pwd
-	cd $(SITE) \
-	&& git add -- mathquill demo.html support downloads downloads.html \
-	&& git commit -m $(SITE_COMMITMSG) \
-	&& git push
-
-$(SITE)/mathquill: $(DIST)
-	mkdir -p $@
-	tar -xzf $(DIST) \
-		--directory $@ \
-		--strip-components=2
-
-$(DIST_DOWNLOAD): $(DIST)
-	mkdir -p $(dir $@)
-	cp $^ $@
-
-# freaking bsd, i swear
-# adapted from https://developer.apple.com/library/mac/documentation/opensource/Conceptual/ShellScripting/PortingScriptstoMacOSX/PortingScriptstoMacOSX.html#//apple_ref/doc/uid/TP40004268-TP40003517-SW21
-ifeq (x, $(shell echo xy | sed -r 's/(x)y/\1/' 2>/dev/null))
-  # gnu
-  SED = sed -r
-  SED_I = $(SED) -i
-else
-  # bsd
-  SED = sed -E
-  SED_I = $(SED) -i ''
-endif
-
-$(DOWNLOADS_PAGE): $(DIST_DOWNLOAD)
-	@echo Using $(SED)
-	@echo -n updating downloads page...
-	@$(SED_I) \
-		-e '/Latest version:/ s/[0-9]+[.][0-9]+[.][0-9]+/$(VERSION)/g' \
-		$(DOWNLOADS_PAGE)
-	@mkdir -p tmp
-	@ls $(SITE)/downloads/*.tgz \
-		| egrep -o '[0-9]+[.][0-9]+[.][0-9]+' \
-		| fgrep -v $(VERSION) \
-		| sort -rn -t. -k 1,1 -k 2,2 -k 3,3 \
-		| sed 's|.*|<li><a class="prev" href="downloads/mathquill-&.tgz">v&</a></li>|' \
-		> tmp/versions-list.html
-	@$(SED_I) \
-		-e '/<a class="prev"/d' \
-		-e '/<ul id="prev-versions">/ r tmp/versions-list.html' \
-		$(DOWNLOADS_PAGE)
-	@rm tmp/versions-list.html
-	@echo done.
-
-$(SITE)/demo.html: test/demo.html
-	cat $^ \
-	| $(SED) 's:../build/:mathquill/:' \
-	| $(SED) 's:local test page:live demo:' \
-	> $@
-
-$(SITE)/support: test/support
-	rm -rf $@
-	cp -r $^ $@
-
-$(SITE):
-	git clone $(SITE_CLONE_URL) $@
-
-site-pull: $(SITE)
-	cd $(SITE) && git pull
diff --git a/README.md b/README.md
index 7b93213..ff01a2d 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
@@ -279,10 +280,11 @@
 `autoOperatorNames`, a list of the same form (space-delimited letters-only each
 length>=2), and overrides the set of operator names that automatically become
 non-italicized when typing the letters without typing a backslash first, like
-`sin`, `log`, etc. (Defaults to [the LaTeX built-in operator names][Wikia], but
-with additional trig operators like `sech`, `arcsec`, `arsinh`, etc.)
+`sin`, `log`, etc. Defaults to the LaTeX built-in operator names ([Section
+3.17 of the Short Math Guide][3.17]) plus some missing trig operators like
+`sech`, `arcsec`, `arsinh`, etc.
 
-[Wikia]: http://latex.wikia.com/wiki/List_of_LaTeX_symbols#Named_operators:_sin.2C_cos.2C_etc.
+[3.17]: http://tinyurl.com/jm9okjc
 
 `substituteTextarea`, a function that creates a focusable DOM element, called
 when setting up a math field. It defaults to `<textarea autocorrect=off .../>`,
diff --git a/package.json b/package.json
index 703e3ec..9c8fb28 100644
--- a/package.json
+++ b/package.json
@@ -1,17 +1,17 @@
 {
   "name": "mathquill",
   "description": "Easily type math in your webapp",
-  "version": "0.10.0",
+  "version": "0.10.1",
   "license": "MPL-2.0",
   "repository": {
     "type": "git",
     "url": "https://github.com/mathquill/mathquill.git"
   },
   "dependencies": {
-    "pjs": "3.x"
+    "pjs": ">=3.1.0 <5.0.0"
   },
   "devDependencies": {
-    "mocha": "*",
+    "mocha": ">=2.4.1",
     "uglify-js": "2.x",
     "less": ">=1.5.1"
   }
diff --git a/src/commands/math/advancedSymbols.js b/src/commands/math/advancedSymbols.js
index a4dccb4..f5aa5a5 100644
--- a/src/commands/math/advancedSymbols.js
+++ b/src/commands/math/advancedSymbols.js
@@ -197,6 +197,7 @@
 LatexCmds.spadesuit = bind(VanillaSymbol, '\\spadesuit ', '&#9824;');
 //not real LaTex command see https://github.com/mathquill/mathquill/pull/552 for more details
 LatexCmds.parallelogram = bind(VanillaSymbol, '\\parallelogram ', '&#9649;');
+LatexCmds.square = bind(VanillaSymbol, '\\square ', '&#11036;');
 
 //variable-sized
 LatexCmds.oint = bind(VanillaSymbol, '\\oint ', '&#8750;');
@@ -221,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;');
@@ -302,6 +299,9 @@
 LatexCmds.xist = //LOL
 LatexCmds.xists = LatexCmds.exist = LatexCmds.exists =
   bind(VanillaSymbol,'\\exists ','&exist;');
+  
+LatexCmds.nexists = LatexCmds.nexist =
+      bind(VanillaSymbol, '\\nexists ', '&#8708;');
 
 LatexCmds.and = LatexCmds.land = LatexCmds.wedge =
   bind(VanillaSymbol,'\\wedge ','&and;');
@@ -319,7 +319,8 @@
 LatexCmds.cap = LatexCmds.intersect = LatexCmds.intersection =
   bind(BinaryOperator,'\\cap ','&cap;');
 
-LatexCmds.deg = LatexCmds.degree = bind(VanillaSymbol,'^\\circ ','&deg;');
+// FIXME: the correct LaTeX would be ^\circ but we can't parse that
+LatexCmds.deg = LatexCmds.degree = bind(VanillaSymbol,'\\degree ','&deg;');
 
 LatexCmds.ang = LatexCmds.angle = bind(VanillaSymbol,'\\angle ','&ang;');
 LatexCmds.measuredangle = bind(VanillaSymbol,'\\measuredangle ','&#8737;');
diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js
index ec15b9a..a8578ae 100644
--- a/src/commands/math/basicSymbols.js
+++ b/src/commands/math/basicSymbols.js
@@ -59,26 +59,27 @@
 var Letter = P(Variable, function(_, super_) {
   _.init = function(ch) { return super_.init.call(this, this.letter = ch); };
   _.createLeftOf = function(cursor) {
+    super_.createLeftOf.apply(this, arguments);
     var autoCmds = cursor.options.autoCommands, maxLength = autoCmds._maxLength;
     if (maxLength > 0) {
       // want longest possible autocommand, so join together longest
       // sequence of letters
-      var str = this.letter, l = cursor[L], i = 1;
-      while (l instanceof Letter && i < maxLength) {
+      var str = '', l = this, i = 0;
+      // FIXME: l.ctrlSeq === l.letter checks if first or last in an operator name
+      while (l instanceof Letter && l.ctrlSeq === l.letter && i < maxLength) {
         str = l.letter + str, l = l[L], i += 1;
       }
       // check for an autocommand, going thru substrings longest to shortest
       while (str.length) {
         if (autoCmds.hasOwnProperty(str)) {
-          for (var i = 2, l = cursor[L]; i < str.length; i += 1, l = l[L]);
-          Fragment(l, cursor[L]).remove();
+          for (var i = 1, l = this; i < str.length; i += 1, l = l[L]);
+          Fragment(l, this).remove();
           cursor[L] = l[L];
           return LatexCmds[str](str).createLeftOf(cursor);
         }
         str = str.slice(1);
       }
     }
-    super_.createLeftOf.apply(this, arguments);
   };
   _.italicize = function(bool) {
     this.isItalic = bool;
@@ -103,7 +104,7 @@
     // removeClass and delete flags from all letters before figuring out
     // which, if any, are part of an operator name
     Fragment(l[R] || this.parent.ends[L], r[L] || this.parent.ends[R]).each(function(el) {
-      el.italicize(true).jQ.removeClass('mq-first mq-last');
+      el.italicize(true).jQ.removeClass('mq-first mq-last mq-followed-by-supsub');
       el.ctrlSeq = el.letter;
     });
 
@@ -122,8 +123,21 @@
           first.ctrlSeq = (isBuiltIn ? '\\' : '\\operatorname{') + first.ctrlSeq;
           last.ctrlSeq += (isBuiltIn ? ' ' : '}');
           if (TwoWordOpNames.hasOwnProperty(word)) last[L][L][L].jQ.addClass('mq-last');
-          if (nonOperatorSymbol(first[L])) first.jQ.addClass('mq-first');
-          if (nonOperatorSymbol(last[R])) last.jQ.addClass('mq-last');
+          if (!shouldOmitPadding(first[L])) first.jQ.addClass('mq-first');
+          if (!shouldOmitPadding(last[R])) {
+            if (last[R] instanceof SupSub) {
+              var supsub = last[R]; // XXX monkey-patching, but what's the right thing here?
+              // Have operatorname-specific code in SupSub? A CSS-like language to style the
+              // math tree, but which ignores cursor and selection (which CSS can't)?
+              var respace = supsub.siblingCreated = supsub.siblingDeleted = function() {
+                supsub.jQ.toggleClass('mq-after-operator-name', !(supsub[R] instanceof Bracket));
+              };
+              respace();
+            }
+            else {
+              last.jQ.toggleClass('mq-last', !(last[R] instanceof Bracket));
+            }
+          }
 
           i += len - 1;
           first = last;
@@ -132,12 +146,13 @@
       }
     }
   };
-  function nonOperatorSymbol(node) {
-    return node instanceof Symbol && !(node instanceof BinaryOperator);
+  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);
   }
 });
 var BuiltInOpNames = {}; // the set of operator names like \sin, \cos, etc that
-  // are built-into LaTeX: http://latex.wikia.com/wiki/List_of_LaTeX_symbols#Named_operators:_sin.2C_cos.2C_etc.
+  // are built-into LaTeX, see Section 3.17 of the Short Math Guide: http://tinyurl.com/jm9okjc
   // MathQuill auto-unitalicizes some operator names not in that set, like 'hcf'
   // and 'arsinh', which must be exported as \operatorname{hcf} and
   // \operatorname{arsinh}. Note: over/under line/arrow \lim variants like
@@ -407,8 +422,11 @@
 
   _.contactWeld = _.siblingCreated = _.siblingDeleted = function(opts, dir) {
     if (dir === R) return; // ignore if sibling only changed on the right
+    // If the left sibling is a binary operator or a separator (comma, semicolon, colon)
+    // or an open bracket (open parenthesis, open square bracket)
+    // consider the operator to be unary, otherwise binary
     this.jQ[0].className =
-      (!this[L] || this[L] instanceof BinaryOperator ? '' : 'mq-binary-operator');
+      (!this[L] || this[L] instanceof BinaryOperator || /^[,;:\(\[]$/.test(this[L].ctrlSeq) ? '' : 'mq-binary-operator');
     return this;
   };
 });
@@ -422,7 +440,7 @@
   bind(PlusMinus,'\\mp ','&#8723;');
 
 CharCmds['*'] = LatexCmds.sdot = LatexCmds.cdot =
-  bind(BinaryOperator, '\\cdot ', '&middot;');
+  bind(BinaryOperator, '\\cdot ', '&middot;', '*');
 //semantically should be &sdot;, but &middot; looks better
 
 var Inequality = P(BinaryOperator, function(_, super_) {
diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js
index 37c00bf..f4e1381 100644
--- a/src/commands/math/commands.js
+++ b/src/commands/math/commands.js
@@ -181,7 +181,6 @@
         break;
       }
     }
-    this.respace();
   };
   Options.p.charsThatBreakOutOfSupSub = '';
   _.finalizeTree = function() {
@@ -230,10 +229,6 @@
     }
     return latex('_', this.sub) + latex('^', this.sup);
   };
-  _.respace = _.siblingCreated = _.siblingDeleted = function(opts, dir) {
-    if (dir === R) return; // ignore if sibling only changed on the right
-    this.jQ.toggleClass('mq-limit', this[L].ctrlSeq === '\\int ');
-  };
   _.addBlock = function(block) {
     if (this.supsub === 'sub') {
       this.sup = this.upInto = this.sub.upOutOf = block;
@@ -380,6 +375,26 @@
 LatexCmds.coprod =
 LatexCmds.coproduct = bind(SummationNotation,'\\coprod ','&#8720;');
 
+LatexCmds['∫'] =
+LatexCmds['int'] =
+LatexCmds.integral = P(SummationNotation, function(_, super_) {
+  _.init = function() {
+    var htmlTemplate =
+      '<span class="mq-int mq-non-leaf">'
+    +   '<big>&int;</big>'
+    +   '<span class="mq-supsub mq-non-leaf">'
+    +     '<span class="mq-sup"><span class="mq-sup-inner">&1</span></span>'
+    +     '<span class="mq-sub">&0</span>'
+    +     '<span style="display:inline-block;width:0">&#8203</span>'
+    +   '</span>'
+    + '</span>'
+    ;
+    Symbol.prototype.init.call(this, '\\int ', htmlTemplate);
+  };
+  // FIXME: refactor rather than overriding
+  _.createLeftOf = MathCommand.p.createLeftOf;
+});
+
 var Fraction =
 LatexCmds.frac =
 LatexCmds.dfrac =
diff --git a/src/commands/text.js b/src/commands/text.js
index 42f85d4..59614ee 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
@@ -287,7 +296,6 @@
   };
 });
 
-CharCmds.$ =
 LatexCmds.text =
 LatexCmds.textnormal =
 LatexCmds.textrm =
diff --git a/src/css/main.less b/src/css/main.less
index a56bece..25dd7b4 100644
--- a/src/css/main.less
+++ b/src/css/main.less
@@ -1,5 +1,5 @@
 /*
- * MathQuill v0.10.0               http://mathquill.com
+ * MathQuill v0.10.1               http://mathquill.com
  * by Han, Jeanine, and Mary  maintainers@mathquill.com
  *
  * This Source Code Form is subject to the terms of the
diff --git a/src/css/math.less b/src/css/math.less
index 6115ee8..aae409d 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 {
@@ -80,7 +87,29 @@
   }
 
   big {
-    font-size: 125%;
+    font-size: 200%;
+  }
+
+  .mq-int {
+    > big {
+      display: inline-block;
+      .transform(scaleX(.7));
+      vertical-align: -.16em;
+    }
+
+    > .mq-supsub {
+      font-size: 80%;
+      vertical-align: -1.1em;
+      padding-right: .2em;
+
+      > .mq-sup > .mq-sup-inner {
+        vertical-align: 1.3em;
+      }
+
+      > .mq-sub {
+        margin-left: -.35em;
+      }
+    }
   }
 
   .mq-roman {
@@ -122,12 +151,9 @@
   //   its contents, rather than always being as wide as the subscript.
   //   See also .fraction
   .mq-supsub {
+    text-align: left;
     font-size: 90%;
     vertical-align: -.5em;
-    &.mq-limit {
-      font-size: 80%;
-      vertical-align: -.4em;
-    }
 
     &.mq-sup-only {
       vertical-align: .5em;
@@ -146,9 +172,6 @@
       display: block;
       float: left;
     }
-    &.mq-limit .mq-sub {
-      margin-left: -.25em;
-    }
 
     .mq-binary-operator {
       padding: 0 .1em;
@@ -208,7 +231,7 @@
   var.mq-operator-name.mq-first {
     padding-left: .2em;
   }
-  var.mq-operator-name.mq-last {
+  var.mq-operator-name.mq-last, .mq-supsub.mq-after-operator-name {
     padding-right: .2em;
   }
 
@@ -284,6 +307,8 @@
   }
 
   .mq-large-operator {
+    vertical-align: -.2em;
+    padding: .2em;
     text-align: center;
 
     .mq-from, big, .mq-to  {
diff --git a/src/css/mixins/css3.less b/src/css/mixins/css3.less
index 2aecf28..b53bf6b 100644
--- a/src/css/mixins/css3.less
+++ b/src/css/mixins/css3.less
@@ -5,6 +5,13 @@
   -o-transform-origin: @arguments;
   transform-origin: @arguments;
 }
+.transform (...) {
+  -webkit-transform: @arguments;
+  -moz-transform: @arguments;
+  -ms-transform: @arguments;
+  -o-transform: @arguments;
+  transform: @arguments;
+}
 
 .user-select (...) {
   -webkit-user-select: @arguments;
diff --git a/src/css/textarea.less b/src/css/textarea.less
index 487e1f1..1484fa7 100644
--- a/src/css/textarea.less
+++ b/src/css/textarea.less
@@ -14,7 +14,7 @@
     position: absolute; // the only way to hide the textarea *and* the
     clip: rect(1em 1em 1em 1em); // blinking insertion point in IE
 
-    font-size: 0; // the only way to hide the blinking blue cursor in iOS 8
+    .transform(scale(0)); // the only way to hide the blinking blue cursor in iOS 8 #584
 
     resize: none; // hotfix: https://code.google.com/p/chromium/issues/detail?id=355199#c1
 
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/font/Symbola.woff b/src/font/Symbola.woff
old mode 100755
new mode 100644
index b9bba23..858ca28
--- a/src/font/Symbola.woff
+++ b/src/font/Symbola.woff
Binary files differ
diff --git a/src/font/Symbola.woff2 b/src/font/Symbola.woff2
old mode 100755
new mode 100644
index 9d3e820..07a752d
--- a/src/font/Symbola.woff2
+++ b/src/font/Symbola.woff2
Binary files differ
diff --git a/src/intro.js b/src/intro.js
index e1d9379..654b8cd 100644
--- a/src/intro.js
+++ b/src/intro.js
@@ -1,5 +1,5 @@
 /**
- * MathQuill v0.10.0               http://mathquill.com
+ * MathQuill v0.10.1               http://mathquill.com
  * by Han, Jeanine, and Mary  maintainers@mathquill.com
  *
  * This Source Code Form is subject to the terms of the
diff --git a/src/publicapi.js b/src/publicapi.js
index 45b05e5..2ccdeb2 100644
--- a/src/publicapi.js
+++ b/src/publicapi.js
@@ -15,7 +15,7 @@
 function insistOnInterVer() {
   if (window.console) console.warn(
     'You are using the MathQuill API without specifying an interface version, ' +
-    'which will fail in v1.0.0. You can fix this easily by doing this before ' +
+    'which will fail in v1.0.0. Easiest fix is to do the following before ' +
     'doing anything else:\n' +
     '\n' +
     '    MathQuill = MathQuill.getInterface(1);\n' +
@@ -213,6 +213,19 @@
       var cmd = Embed().setOptions(options);
       cmd.createLeftOf(this.__controller.cursor);
     };
+    _.clickAt = function(clientX, clientY, target) {
+      target = target || document.elementFromPoint(clientX, clientY);
+
+      var ctrlr = this.__controller, root = ctrlr.root;
+      if (!jQuery.contains(root.jQ[0], target)) target = root.jQ[0];
+      ctrlr.seek($(target), clientX + pageXOffset, clientY + pageYOffset);
+      if (ctrlr.blurred) this.focus();
+      return this;
+    };
+    _.ignoreNextMousedown = function(fn) {
+      this.__controller.cursor.options.ignoreNextMousedown = fn;
+      return this;
+    };
   });
   MQ.EditableField = function() { throw "wtf don't call me, I'm 'abstract'"; };
   MQ.EditableField.prototype = APIClasses.EditableField.prototype;
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/src/services/mouse.js b/src/services/mouse.js
index c060461..b2b543a 100644
--- a/src/services/mouse.js
+++ b/src/services/mouse.js
@@ -3,6 +3,7 @@
  *******************************************************/
 
 Controller.open(function(_) {
+  Options.p.ignoreNextMousedown = noop;
   _.delegateMouseEvents = function() {
     var ultimateRootjQ = this.root.jQ;
     //drag-to-select event handling
@@ -12,6 +13,12 @@
       var ctrlr = root.controller, cursor = ctrlr.cursor, blink = cursor.blink;
       var textareaSpan = ctrlr.textareaSpan, textarea = ctrlr.textarea;
 
+      e.preventDefault(); // doesn't work in IE≤8, but it's a one-line fix:
+      e.target.unselectable = true; // http://jsbin.com/yagekiji/1
+
+      if (cursor.options.ignoreNextMousedown(e)) return;
+      else cursor.options.ignoreNextMousedown = noop;
+
       var target;
       function mousemove(e) { target = $(e.target); }
       function docmousemove(e) {
@@ -42,8 +49,6 @@
         if (!ctrlr.editable) rootjQ.prepend(textareaSpan);
         textarea.focus();
       }
-      e.preventDefault(); // doesn't work in IE≤8, but it's a one-line fix:
-      e.target.unselectable = true; // http://jsbin.com/yagekiji/1
 
       cursor.blink = noop;
       ctrlr.seek($(e.target), e.pageX, e.pageY).cursor.startSelection();
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/css.test.js b/test/unit/css.test.js
index 223895f..639cdf9 100644
--- a/test/unit/css.test.js
+++ b/test/unit/css.test.js
@@ -52,5 +52,58 @@
     var mqF = $(mq.el()).find('.mq-f');
     var testVal = parseFloat(mqF.css('margin-right')) - parseFloat(mqF.css('margin-left'));
     assert.ok(testVal > 0, 'this should be truthy') ;
+
+    $(mq.el()).remove();
+  });
+
+  test('unary PlusMinus before separator', function () {
+    var mq = MQ.MathField($('<span></span>').appendTo('#mock')[0]);
+    mq.latex('(-1,-1-1)-1,(+1;+1+1)+1,(\\pm1,\\pm1\\pm1)\\pm1');
+    var spans = $(mq.el()).find('.mq-root-block').find('span');
+    assert.equal(spans.length, 35, 'PlusMinus expression parsed incorrectly');
+
+    function isBinaryOperator(i) { return $(spans[i]).hasClass('mq-binary-operator'); }
+    function assertBinaryOperator(i, s) { assert.ok(isBinaryOperator(i), '"' + s + '" should be binary'); }
+    function assertUnaryOperator(i, s) { assert.ok(!isBinaryOperator(i), '"' + s + '" should be unary'); }
+
+    assertUnaryOperator(1, '(-');
+    assertUnaryOperator(4, '(-1,-');
+    assertBinaryOperator(6, '(-1,-1-');
+    assertBinaryOperator(9, '(-1,-1-1)-');
+    assertUnaryOperator(13, '(-1,-1-1)-1,(+');
+    assertUnaryOperator(16, '(-1,-1-1)-1,(+1;+');
+    assertBinaryOperator(18, '(-1,-1-1)-1,(+1;+1+');
+    assertBinaryOperator(21, '(-1,-1-1)-1,(+1;+1+1)+');
+    assertUnaryOperator(25, '(-1,-1-1)-1,(+1;+1+1)+1,(\pm');
+    assertUnaryOperator(28, '(-1,-1-1)-1,(+1;+1+1)+1,(\pm1,\pm');
+    assertBinaryOperator(30, '(-1,-1-1)-1,(+1;+1+1)+1,(\pm1,\pm1\pm');
+    assertBinaryOperator(33, '(-1,-1-1)-1,(+1;+1+1)+1,(\pm1,\pm1\pm1)\pm');
+
+    $(mq.el()).remove();
+  });
+
+  test('operator name spacing e.g. sin x', function() {
+    var mq = MathQuill.MathField($('<span></span>').appendTo(mock)[0]);
+
+    mq.typedText('sin');
+    var n = jQuery('#mock var.mq-operator-name:last');
+    assert.equal(n.text(), 'n');
+    assert.ok(!n.is('.mq-last'));
+
+    mq.typedText('x');
+    assert.ok(n.is('.mq-last'));
+
+    mq.keystroke('Left').typedText('(');
+    assert.ok(!n.is('.mq-last'));
+
+    mq.keystroke('Backspace').typedText('^');
+    assert.ok(!n.is('.mq-last'));
+    var supsub = jQuery('#mock .mq-supsub');
+    assert.ok(supsub.is('.mq-after-operator-name'));
+
+    mq.typedText('2').keystroke('Tab').typedText('(');
+    assert.ok(!supsub.is('.mq-after-operator-name'));
+
+    $(mq.el()).empty();
   });
 });
diff --git a/test/unit/focusBlur.test.js b/test/unit/focusBlur.test.js
index 1bd3af9..27180ae 100644
--- a/test/unit/focusBlur.test.js
+++ b/test/unit/focusBlur.test.js
@@ -1,6 +1,6 @@
 suite('focusBlur', function() {
   function assertHasFocus(mq, name, invert) {
-    assert.ok(!!invert ^ $(mq.el()).find('textarea').is(':focus'), name + (invert ? ' does not have focus' : ' has focus'));
+    assert.ok(!!invert ^ ($(mq.el()).find('textarea')[0] === document.activeElement), name + (invert ? ' does not have focus' : ' has focus'));
   }
 
   suite('handlers can shift focus away', function() {
diff --git a/test/unit/latex.test.js b/test/unit/latex.test.js
index 5795b18..4cf59fa 100644
--- a/test/unit/latex.test.js
+++ b/test/unit/latex.test.js
@@ -100,11 +100,14 @@
     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() {
     assertParsesLatex('\\parallelogram ');
     assertParsesLatex('\\circledot ', '\\odot ');
+    assertParsesLatex('\\degree ');
+    assertParsesLatex('\\square ');
   });
 
   suite('public API', function() {
diff --git a/test/unit/publicapi.test.js b/test/unit/publicapi.test.js
index efeb9c2..91ef1f0 100644
--- a/test/unit/publicapi.test.js
+++ b/test/unit/publicapi.test.js
@@ -124,7 +124,7 @@
       mq.typedText('qrt');
       assert.equal(mq.text(), '\\sqrt');
     });
-    
+
     test('.text() with complete commands', function() {
       mq.latex('\\sqrt{}');
       assert.equal(mq.text(), 'sqrt()');
@@ -145,7 +145,11 @@
       mq.latex('3x+\\ 4');
       assert.equal(mq.text(), '3*x+ 4');
       mq.latex('x^2');
-      assert.equal(mq.text(), 'x^2')
+      assert.equal(mq.text(), 'x^2');
+
+      mq.latex('');
+      mq.typedText('*2*3***4');
+      assert.equal(mq.text(), '*2*3***4');
     });
 
     test('.moveToDirEnd(dir)', function() {
@@ -321,6 +325,8 @@
       assert.equal(mq.latex(), 'xy^2');
       mq.keystroke('Right Shift-Left Shift-Left Shift-Left').cmd('\\sqrt');
       assert.equal(mq.latex(), '\\sqrt{xy^2}');
+      mq.typedText('*2**');
+      assert.equal(mq.latex(), '\\sqrt{xy^2\\cdot2\\cdot\\cdot}');
     });
 
     test('backslash commands are passed their name', function() {
@@ -738,6 +744,20 @@
 
       $(mq.el()).remove();
     });
+    test('integral still has empty limits', function() {
+      var mq = MQ.MathField($('<span>').appendTo('#mock')[0], {
+        sumStartsWithNEquals: true
+      });
+      assert.equal(mq.latex(), '');
+
+      mq.cmd('\\int');
+      assert.equal(mq.latex(), '\\int_{ }^{ }');
+
+      mq.cmd('0');
+      assert.equal(mq.latex(), '\\int_0^{ }', 'cursor in the from block');
+
+      $(mq.el()).remove();
+    });
   });
 
   suite('substituteTextarea', function() {
@@ -756,6 +776,58 @@
     });
   });
 
+  suite('clickAt', function() {
+    test('inserts at coordinates', function() {
+      // Insert filler so that the page is taller than the window so this test is deterministic
+      // Test that we use clientY instead of pageY
+      var windowHeight = $(window).height();
+      var filler = $('<div>').height(windowHeight);
+      filler.insertBefore('#mock');
+
+      var mq = MQ.MathField($('<span>').appendTo('#mock')[0]);
+      mq.typedText("mmmm/mmmm");
+      mq.el().scrollIntoView();
+
+      var box = mq.el().getBoundingClientRect();
+      var clientX = box.left + 30;
+      var clientY = box.top + 40;
+      var target = document.elementFromPoint(clientX, clientY);
+
+      assert.equal(document.activeElement, document.body);
+      mq.clickAt(clientX, clientY, target).write('x');
+      assert.equal(document.activeElement, $(mq.el()).find('textarea')[0]);
+
+      assert.equal(mq.latex(), "\\frac{mmmm}{mmxmm}");
+
+      filler.remove();
+      $(mq.el()).remove();
+    });
+    test('target is optional', function() {
+      // Insert filler so that the page is taller than the window so this test is deterministic
+      // Test that we use clientY instead of pageY
+      var windowHeight = $(window).height();
+      var filler = $('<div>').height(windowHeight);
+      filler.insertBefore('#mock');
+
+      var mq = MQ.MathField($('<span>').appendTo('#mock')[0]);
+      mq.typedText("mmmm/mmmm");
+      mq.el().scrollIntoView();
+
+      var box = mq.el().getBoundingClientRect();
+      var clientX = box.left + 30;
+      var clientY = box.top + 40;
+
+      assert.equal(document.activeElement, document.body);
+      mq.clickAt(clientX, clientY).write('x');
+      assert.equal(document.activeElement, $(mq.el()).find('textarea')[0]);
+
+      assert.equal(mq.latex(), "\\frac{mmmm}{mmxmm}");
+
+      filler.remove();
+      $(mq.el()).remove();
+    });
+  });
+
   suite('dropEmbedded', function() {
     test('inserts into empty', function() {
       var mq = MQ.MathField($('<span>').appendTo('#mock')[0]);
diff --git a/test/unit/saneKeyboardEvents.test.js b/test/unit/saneKeyboardEvents.test.js
index 9247803..158d425 100644
--- a/test/unit/saneKeyboardEvents.test.js
+++ b/test/unit/saneKeyboardEvents.test.js
@@ -146,42 +146,42 @@
       assert.ok(document.activeElement !== el[0], 'textarea remains blurred');
     });
 
-    if (!document.hasFocus()) {
-      test('blur in keystroke handler: DOCUMENT NEEDS FOCUS, SEE CONSOLE ');
-      console.warn(
-        'The test "blur in keystroke handler" needs the document to have ' +
-        'focus. Only when the document has focus does .select() on an ' +
-        'element also focus it, which is part of the problematic behavior ' +
-        'we are testing robustness against. (Specifically, erroneously ' +
-        'calling .select() in a timeout after the textarea has blurred, ' +
-        '"stealing back" focus.)\n' +
-        'Normally, the page being open and focused is enough to have focus, ' +
-        'but with the Developer Tools open, it depends on whether you last ' +
-        'clicked on something in the Developer Tools or on the page itself. ' +
-        'Click the page, or close the Developer Tools, and Refresh.'
-      );
-    }
-    else {
-      test('blur in keystroke handler', function(done) {
-        var shim = saneKeyboardEvents(el, {
-          keystroke: function(key) {
-            assert.equal(key, 'Left');
-            el[0].blur();
-          }
-        });
+    test('blur in keystroke handler', function(done) {
+      if (!document.hasFocus()) {
+        console.warn(
+          'The test "blur in keystroke handler" needs the document to have ' +
+          'focus. Only when the document has focus does .select() on an ' +
+          'element also focus it, which is part of the problematic behavior ' +
+          'we are testing robustness against. (Specifically, erroneously ' +
+          'calling .select() in a timeout after the textarea has blurred, ' +
+          '"stealing back" focus.)\n' +
+          'Normally, the page being open and focused is enough to have focus, ' +
+          'but with the Developer Tools open, it depends on whether you last ' +
+          'clicked on something in the Developer Tools or on the page itself. ' +
+          'Click the page, or close the Developer Tools, and Refresh.'
+        );
+        el.remove(); // LOL next line skips teardown https://git.io/vaUWq
+        this.skip();
+      }
 
-        shim.select('foobar');
-        assert.ok(document.activeElement === el[0], 'textarea focused');
-
-        el.trigger(Event('keydown', { which: 37 }));
-        assert.ok(document.activeElement !== el[0], 'textarea blurred');
-
-        setTimeout(function() {
-          assert.ok(document.activeElement !== el[0], 'textarea remains blurred');
-          done();
-        });
+      var shim = saneKeyboardEvents(el, {
+        keystroke: function(key) {
+          assert.equal(key, 'Left');
+          el[0].blur();
+        }
       });
-    }
+
+      shim.select('foobar');
+      assert.ok(document.activeElement === el[0], 'textarea focused');
+
+      el.trigger(Event('keydown', { which: 37 }));
+      assert.ok(document.activeElement !== el[0], 'textarea blurred');
+
+      setTimeout(function() {
+        assert.ok(document.activeElement !== el[0], 'textarea remains blurred');
+        done();
+      });
+    });
 
     suite('selected text after keypress or paste doesn\'t get mistaken' +
          ' for inputted text', 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();
+    }
+  });
 });
diff --git a/test/unit/typing.test.js b/test/unit/typing.test.js
index 3ba3b31..737d73f 100644
--- a/test/unit/typing.test.js
+++ b/test/unit/typing.test.js
@@ -56,6 +56,11 @@
       mq.typedText('\\asdf+');
       assertLatex('\\text{asdf}+');
     });
+
+    test('dollar sign', function() {
+      mq.typedText('$');
+      assertLatex('\\$');
+    });
   });
 
   suite('auto-expanding parens', function() {
@@ -813,6 +818,7 @@
   suite('autoCommands', function() {
     setup(function() {
       MQ.config({
+        autoOperatorNames: 'sin pp',
         autoCommands: 'pi tau phi theta Gamma sum prod sqrt nthroot'
       });
     });
@@ -880,6 +886,13 @@
       assertLatex('\\sin\\pi');
     });
 
+    test('has lower "precedence" than operator names', function() {
+      mq.typedText('ppi');
+      assertLatex('\\operatorname{pp}i');
+      mq.keystroke('Left Left').typedText('i');
+      assertLatex('\\pi pi');
+    });
+
     test('command contains non-letters', function() {
       assert.throws(function() { MQ.config({ autoCommands: 'e1' }); });
     });
diff --git a/test/visual.html b/test/visual.html
index 1a47eca..f066a7b 100644
--- a/test/visual.html
+++ b/test/visual.html
@@ -24,6 +24,9 @@
 td {

   width: 33%;

 }

+#static-latex-rendering-table td {

+  width: 50%;

+}

 #show-textareas-button {

   float: right;

 }

@@ -77,14 +80,14 @@
   <td><span class="mathquill-static-math">\sqrt{\MathQuillMathField{x^2+y^2}}</span>

 </table>

 

-<p>Clicks/mousedown to drag should work anywhere in the blue box: <div class="math-container" style="border: solid 1px lightblue; height: 5em; width: 15em; line-height: 5em; text-align: center"><span class="mathquill-math-field">a_2 x^2 + a_1 x + a_0 = 0</span></div>

+<p>Touch taps/clicks/mousedown to drag should work anywhere in the blue box: <div class="math-container" style="border: solid 1px lightblue; height: 5em; width: 15em; line-height: 5em; text-align: center; -webkit-tap-highlight-color: rgba(0,0,0,0)"><span class="mathquill-math-field">x_{very\ long\ thing}^2 + a_0 = 0</span></div>

 

 <h3>Redrawing</h3>

 <p>

-  <span id="reflowing-test">\sqrt{_0^1}</span>

+  <span id="reflowing-test">\sqrt{}</span>

   should look the same as

   <span class="mathquill-static-math">

-    \sqrt{\pi\sqrt\sqrt\frac12\int_0^1}

+    \sqrt{\pi\sqrt\sqrt\frac12}

   </span>

 </p>

 <script type="text/javascript">

@@ -96,7 +99,7 @@
     var textarea = $('#reflowing-test textarea');

     // paste some stuff that needs resizing

     textarea.trigger('paste');

-    textarea.val('\\pi\\sqrt{\\sqrt{\\frac12}}\\int');

+    textarea.val('\\pi\\sqrt{\\sqrt{\\frac12}}');

     setTimeout(function() { if (count !== 1) throw 'reflow not called'; });

   });

 </script>

@@ -183,11 +186,22 @@
 </table>

 

 <h3>Static LaTeX rendering (<code>.mathquill-static-math</code>) tests</h3>

-<table>

+<table id="static-latex-rendering-table">

 <tr><td><span class="mathquill-static-math">^{\frac{as}{ }df}</span><td><span>^{\frac{as}{ }df}</span>

 <tr><td><span class="mathquill-static-math">e^{i\pi}+1=0</span><td><span>e^{i\pi}+1=0</span>

 <tr><td><span class="mathquill-static-math">\sqrt[n]{1}</span><td><span>\sqrt[n]{1}</span>

-<tr><td><span class="mathquill-static-math">\theta + \sin 2\deg</span><td><span>\theta + \sin 2\deg</span>

+<tr><td><span class="mathquill-static-math">\sin ^2x+\sin ^2\left(x\right)+\sin ^2(x)</span></td><td><span>\sin ^2x+\sin ^2\left(x\right)+\sin ^2(x)</span>

+<tr><td><span class="mathquill-static-math">12a\sin b</span></td><td><span>12a\sin b</span>

+<tr><td><span class="mathquill-static-math">1a^2 \sin b</span></td><td><span>1a^2 \sin b</span>

+<tr><td><span class="mathquill-static-math">a + \sin b</span></td><td><span>a + \sin b</span>

+<tr><td><span class="mathquill-static-math">a + b</span></td><td><span>a + b</span>

+<tr><td><span class="mathquill-static-math">\sum\sin</span></td><td><span>\sum\sin</span>

+<tr><td><span class="mathquill-static-math">\sum a</span></td><td><span>\sum a</span>

+<tr><td><span class="mathquill-static-math">(\sin)</span></td><td><span>(\sin)</span>

+<tr><td><span class="mathquill-static-math">\left(\sin\right)</span></td><td><span>\left(\sin\right)</span>

+<tr><td><span class="mathquill-static-math">(x)\sin(x)</span></td><td><span>(x)\sin(x)</span>

+<tr><td><span class="mathquill-static-math">\left(x\right)\sin\left(x\right)</span></td><td><span>\left(x\right)\sin\left(x\right)</span>

+<tr><td><span class="mathquill-static-math">a \sin b</span></td><td><span>a \sin b</span>

 <tr><td><span class="mathquill-static-math">-1 + +-2</span><td><span>-1 + +-2</span>

 <tr><td><span class="mathquill-static-math">\left ( n+1 \right ) + \frac{1}{\frac{n}{k}} + \binom{n}{k}</span><td><span>\left ( n+1 \right ) + \frac{1}{\frac{n}{k}} + \binom{n}{k}</span>

 <tr><td><span class="mathquill-static-math">x_{\frac{1}{\frac{2}{3}}}^{\frac{\frac{1}{2}}{3}}</span><td><span>x_{\frac{1}{\frac{2}{3}}}^{\frac{\frac{1}{2}}{3}}</span>

@@ -197,6 +211,22 @@
 <tr><td><span class="mathquill-static-math">1+\sum_0^n+\sum_{i=0123}^n+\sum_0^{wordiness}</span><td><span>1+\sum_0^n+\sum_{i=0123}^n+\sum_0^{wordiness}</span>^M

 <tr><td><span class="mathquill-static-math">x\ \ \ +\ \ \ y</span><td><span>x\ \ \ +\ \ \ y</span>^M

 <tr><td><span class="mathquill-static-math">\sum _{n=0}^3\cos x</span><td><span>\sum _{n=0}^3\cos x</span>^M

+<tr><td><span class="mathquill-static-math">\int _{\phi =0}^{2\pi }\int _{\theta =0}^{\pi }\int _{r=0}^{\infty }f(r,\theta ,\phi )r^2\sin \theta drd\theta d\phi </span><td><span>\int _{\phi =0}^{2\pi }\int _{\theta =0}^{\pi }\int _{r=0}^{\infty }f(r,\theta ,\phi )r^2\sin \theta drd\theta d\phi </span>

+<tr><td><span class="mathquill-static-math">\int_0^{\frac{\frac{1}{2}}{3}} \int_0^{\frac{1}{\frac{2}{3}}} \int_0^{\frac{1}{\frac{2}{\frac{3}{\frac{4}{5}}}}}</span><td><span>\int_0^{\frac{\frac{1}{2}}{3}} \int_0^{\frac{1}{\frac{2}{3}}} \int_0^{\frac{1}{\frac{2}{\frac{3}{\frac{4}{5}}}}}</span>

+<tr><td colspan=2><span id="sixes"></span>

+<script>

+$(function() {

+    var i,

+        sixes = '',

+        start = 10,

+        end = 40;

+    for (i = start; i <= end; i++) {

+        sixes += '<span style="font-size: ' + i + 'px" class="mathquill-math-field">6<\/span>';

+    }

+    $('#sixes').html(sixes);

+    $('#sixes .mathquill-math-field').each(function() { MQ.MathField(this); });

+});

+</script>

 </table>

 

 <p id="paren-alignment">Parentheses vertical alignment at font sizes ranging from 10px to 24px: <button>Click Me</button></p>

@@ -272,7 +302,25 @@
 // test selecting from outside the mathquill editable

 var $mq = $('.math-container .mathquill-math-field');

 $('.math-container').mousedown(function(e) {

-  if (!jQuery.contains($mq[0], e.target)) $mq.triggerHandler(e);

+  if (e.target === $mq[0] || $.contains($mq[0], e.target)) return;

+  $mq.triggerHandler(e);

+})

+// test API for "fast touch taps" #622 & #403

+.on('touchstart', function() {

+  var moved = false;

+  $(this).on('touchmove.tmp', function() { moved = true; })

+  .on('touchend.tmp', function(e) {

+    $(this).off('.tmp');

+    MathQuill($mq[0]).ignoreNextMousedown(function() {

+      return Date.now() < e.timeStamp + 1000;

+    });

+    if (moved) return; // note that this happens after .ignoreNextMousedown()

+      // because even if the touch gesture doesn't 'count' as a tap to us,

+      // we still want to suppress the legacy mouse events, else we'd react

+      // fast to some taps and slow to others, that'd be weird

+    var touch = e.originalEvent.changedTouches[0];

+    MathQuill($mq[0]).clickAt(touch.clientX, touch.clientY, touch.target);

+  });

 });

 

 // Selection Tests