Merge pull request #671 from mathquill/fix.ctrlDeleteDir

Fix undefined reference in ctrlDeleteDir.
diff --git a/README.md b/README.md
deleted file mode 120000
index e892330..0000000
--- a/README.md
+++ /dev/null
@@ -1 +0,0 @@
-docs/index.md
\ No newline at end of file
diff --git a/docs/index.md b/README.md
similarity index 100%
rename from docs/index.md
rename to README.md
diff --git a/circle.yml b/circle.yml
index fcb7b97..f93227b 100644
--- a/circle.yml
+++ b/circle.yml
@@ -35,21 +35,32 @@
   cache_directories:
     - ~/sauce-connect
   pre:
+    - "test $SAUCE_USERNAME && test $SAUCE_ACCESS_KEY
+       # Sauce Labs credentials required. Sign up here: https://saucelabs.com/opensauce/"
     - ? |-
-        mkdir -p ~/sauce-connect
-        cd ~/sauce-connect
-        if [ ! -x sc-*-linux/bin/sc ]
-        then
-          wget https://saucelabs.com/downloads/sc-latest-linux.tar.gz
-          tar -xzf sc-latest-linux.tar.gz
-        fi
-        sc-*-linux/bin/sc --user $SAUCE_USERNAME --api-key $SAUCE_ACCESS_KEY --readyfile ~/sauce_is_ready
+        {
+          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
+          # Sauce Connect randomly fails so try twice
+          time sc-*-linux/bin/sc --user $SAUCE_USERNAME --api-key $SAUCE_ACCESS_KEY --readyfile ~/sauce_is_ready \
+            || time sc-*-linux/bin/sc --user $SAUCE_USERNAME --api-key $SAUCE_ACCESS_KEY --readyfile ~/sauce_is_ready \
+            || echo ERROR > ~/sauce_is_ready
+        } >$CIRCLE_ARTIFACTS/sauce-connect.log 2>&1
       :
         background: true
 
 test:
   override:
-    - make server:
+    # Sauce can connect to Safari on ports 3000, 4000, 7000, and 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 >$CIRCLE_ARTIFACTS/make_server.log 2>&1:
         background: true
 
     # CircleCI expects test results to be reported in an JUnit/xUnit-style XML
@@ -59,29 +70,41 @@
     # 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/mocha/xunit.xml
+    - mkdir -p $CIRCLE_TEST_REPORTS/mocha
     - ? |-
-        mkdir -p $CIRCLE_TEST_REPORTS/mocha
-        node << 'EOF' | tee $CIRCLE_TEST_REPORTS/mocha/xunit.xml
-        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);
+        node << 'EOF' >$CIRCLE_TEST_REPORTS/mocha/xunit.xml \
+                      2>$CIRCLE_ARTIFACTS/mocha-test-report.log
+          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/');
         EOF
       :
         background: true
 
     # Wait for tunnel to be ready (`make server` and the trivial Node server
     # are much faster, no need to wait for them)
-    - while [ ! -e ~/sauce_is_ready ]; do sleep 1; done
+    - while [ ! -e ~/sauce_is_ready ]; do sleep 1; done; test "$(<~/sauce_is_ready)" != ERROR
+
+    # Start taking screenshots in the background while the unit tests are running
+    - ? |-
+        {
+          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 ERROR > ~/screenshots_are_ready:
+        } >$CIRCLE_ARTIFACTS/screenshots.log 2>&1
+      :
+        background: true
 
     # Run in-browser unit tests, based on:
     #   https://wiki.saucelabs.com/display/DOCS/JavaScript+Unit+Testing+Methods
     # "build" and "tag" parameters from:
     #   https://wiki.saucelabs.com/display/DOCS/Test+Configuration+Options#TestConfigurationOptions-TestAnnotation
     - |-
-      curl -i https://saucelabs.com/rest/v1/mathquill/js-tests \
+      curl -i https://saucelabs.com/rest/v1/$SAUCE_USERNAME/js-tests \
            -X POST \
            -u $SAUCE_USERNAME:$SAUCE_ACCESS_KEY \
            -H 'Content-Type: application/json' \
@@ -92,7 +115,7 @@
                    "circle-ci"
                  ],
                  "framework": "mocha",
-                 "url": "http://localhost:9292/test/unit.html?post_xunit_to=http://localhost:9000",
+                 "url": "http://localhost:8000/test/unit.html?post_xunit_to=http://localhost:9000",
                  "platforms": [["", "Chrome", ""]]
       }' | tee js-tests
 
@@ -106,7 +129,7 @@
       while true  # Bash has no do...while >:(
       do
         sleep 5
-        curl -i https://saucelabs.com/rest/v1/mathquill/js-tests/status \
+        curl -i https://saucelabs.com/rest/v1/$SAUCE_USERNAME/js-tests/status \
              -X POST \
              -u $SAUCE_USERNAME:$SAUCE_ACCESS_KEY \
              -H 'Content-Type: application/json' \
@@ -118,6 +141,59 @@
         [ "$(node -p 'require("./status.json").completed')" != false ] && break
       done
 
+    # Wait for screenshots to be ready
+    - while [ ! -e ~/screenshots_are_ready ]; do sleep 1; done; test "$(<~/screenshots_are_ready)" != ERROR:
+        timeout: 300
+
+    # Stitch together images
+    # TODO: Split this into multiple yaml lines. I (Michael)
+    #       niavely tried to split this into mutiple yaml lines
+    #       but was unsucessful in doing do.
+    - |-
+      img_dir=$CIRCLE_ARTIFACTS/imgs/
+      for x in $(ls $img_dir)
+      do
+        convert $img_dir/$x/*.png -append $img_dir/$x.png
+      done
+
+      # Remove all directories in $CIRCLE_ARTIFACTS/img
+      # Currently the pieces aren't kept around. If it's
+      # desirable to keep them around, we should use
+      #    cp -r $dir $CIRCLE_ARTIFACTS/img_pieces
+      # The reason the image pieces aren't currently kept
+      # around is that it was leading to a problem. Specifically,
+      # when we get the previous images, we niavely grab any *.png,
+      # including the pieces images. This compounded so that each
+      # iteration of a test run would have all of the images from
+      # the previous test run plus whichever new images were generated.
+      rm -R -- $img_dir/*/
+
+      # Install utility we need
+      npm install -g json
+
+      # Download the latest mathquill artifacts.
+      curl $(curl https://circleci.com/api/v1/project/mathquill/mathquill/latest/artifacts \
+              | json -a url pretty_path -d '\n\t' \
+              | grep '\.png$' \
+              | grep -v 'PREV' \
+              | sed "s:\$CIRCLE_ARTIFACTS/imgs/:-o $img_dir/PREV_:")
+
+      # Generate image diffs.
+      cd $img_dir
+      for file in $(ls PINNED*); do
+        prev=PREV_$file
+        metric_diff=$(compare -metric AE -compose src $prev $file raw_diff.png)
+        composite -alpha on raw_diff.png $prev DIFF_$file
+      done
+
+      for file in $(ls EVERGREEN*); do
+        prev=$(ls PREV_$file*)
+        metric_diff=$(compare -metric AE -compose src $prev $file raw_diff.png)
+        composite -alpha on raw_diff.png $prev DIFF_$file
+      done
+
+      rm raw_diff.png
+
     # finally, complain to Circle CI if there were nonzero test failures
     - |-
       [ "$(node -p 'require("./status.json")["js tests"][0].result.failures')" == 0 ]
diff --git a/docs/Api_Methods.md b/docs/Api_Methods.md
index 8271945..b7db00c 100644
--- a/docs/Api_Methods.md
+++ b/docs/Api_Methods.md
@@ -45,7 +45,7 @@
 ```html
 <span id="fill-in-the-blank">\sqrt{ \MathQuillMathField{x}^2 + \MathQuillMathField{y}^2 }</span>
 <script>
-  var fillInTheBlank = MQ.StaticMath(document.getElementById('#fill-in-the-blank'));
+  var fillInTheBlank = MQ.StaticMath(document.getElementById('fill-in-the-blank'));
   fillInTheBlank.innerFields[0].latex() // => 'x'
   fillInTheBlank.innerFields[1].latex() // => 'y'
 </script>
diff --git a/docs/README.md b/docs/README.md
index dd0ea36..32d46ee 120000
--- a/docs/README.md
+++ b/docs/README.md
@@ -1 +1 @@
-index.md
\ No newline at end of file
+../README.md
\ No newline at end of file
diff --git a/docs/index.md b/docs/index.md
new file mode 120000
index 0000000..32d46ee
--- /dev/null
+++ b/docs/index.md
@@ -0,0 +1 @@
+../README.md
\ No newline at end of file
diff --git a/script/screenshots.js b/script/screenshots.js
new file mode 100644
index 0000000..a28f035
--- /dev/null
+++ b/script/screenshots.js
@@ -0,0 +1,199 @@
+// This script assumes the following:
+//   1. You've installed wd with `npm install wd'.
+//   2. You've set the environment variables $SAUCE_USERNAME and $SAUCE_ACCESS_KEY.
+//   3. If the environment variable $CIRCLE_ARTIFACTS is not set images will be saved in /tmp
+//
+// This scripts creates following files for each browser in browserVersions:
+//    $CIRCLE_ARTIFACTS/imgs/{browser_version_platform}/#.png
+//
+// The intention of this script is that it will be ran from CircleCI
+//
+// Example usage:
+//   node screenshots.js http://localhost:9292/test/visual.html
+//   node screenshots.js http://google.com
+
+var wd = require('wd');
+var fs = require('fs');
+var username = process.env['SAUCE_USERNAME'];
+var accessKey = process.env['SAUCE_ACCESS_KEY'];
+var baseDir = process.env['CIRCLE_ARTIFACTS'] || '/tmp';
+var url = process.argv[2];
+var allImgsDir = baseDir+'/imgs';
+fs.mkdirSync(allImgsDir);
+
+var browserVersions = [
+  {
+    'version': {
+      // Expecting IE 8
+      'browserName': 'Internet Explorer',
+      'platform': 'Windows XP'
+    },
+    'pinned': 'PINNED'
+  },
+  {
+    'version': {
+      // Expecting IE 11
+      'browserName': 'Internet Explorer',
+      'platform': 'Windows 7'
+    },
+    'pinned': 'PINNED'
+  },
+  {
+    'version': {
+      'browserName': 'MicrosoftEdge',
+      'platform': 'Windows 10'
+    },
+    'pinned': 'EVERGREEN'
+  },
+  {
+    'version': {
+      'browserName': 'Firefox',
+      'platform': 'OS X 10.11'
+    },
+    'pinned': 'EVERGREEN'
+  },
+  {
+    'version': {
+      'browserName': 'Safari',
+      'platform': 'OS X 10.11'
+    },
+    'pinned': 'EVERGREEN'
+  },
+  {
+    'version': {
+      'browserName': 'Chrome',
+      'platform': 'OS X 10.11'
+    },
+    'pinned': 'EVERGREEN'
+  },
+  {
+    'version': {
+      'browserName': 'Firefox',
+      'platform': 'Linux'
+    },
+    'pinned': 'EVERGREEN'
+  },
+];
+
+
+browserVersions.forEach(function(obj) {
+  var cfg = obj.version;
+  var browserDriver = wd.remote('ondemand.saucelabs.com', 80, username, accessKey);
+  // The following is in the style of
+  // https://github.com/admc/wd/blob/62f2b0060d36a402de5634477b26a5ed4c051967/examples/async/chrome.js#L25-L40
+  browserDriver.init(cfg, function(err, _, capabilities) {
+    if (err) console.log(err);
+    console.log(cfg.browserName,cfg.platform,'init')
+
+    var browser = cfg.browserName.replace(/\s/g, '_');
+    var platform = cfg.platform.replace(/\s/g, '_');
+    var piecesDir = allImgsDir+'/'+obj.pinned+'_'+platform+'_'+browser;
+    fs.mkdirSync(piecesDir);
+
+    browserDriver.get(url, function(err) {
+      if (err) console.log(err);
+      console.log(cfg.browserName,cfg.platform,'get')
+      browserDriver.safeExecute('document.documentElement.scrollHeight', function(err,scrollHeight) {
+        if (err) console.log(err);
+        console.log(cfg.browserName,cfg.platform,'get scrollHeight')
+        browserDriver.safeExecute('document.documentElement.clientHeight', function(err,viewportHeight) {
+          if (err) console.log(err);
+          console.log(cfg.browserName,cfg.platform,'get clientHeight')
+
+          // Firefox and Internet Explorer will take a screenshot of the entire webpage,
+          if (cfg.browserName != 'Safari' && cfg.browserName != 'Chrome' && cfg.browserName != 'MicrosoftEdge') {
+            // saves file in the file `piecesDir/browser_version_platform/*.png`
+            var filename = piecesDir+'/'+browser+'_'+platform+'.png';
+            browserDriver.saveScreenshot(filename, function(err) {
+              if (err) console.log(err);
+              console.log(cfg.browserName,cfg.platform,'saveScreenshot');
+
+              browserDriver.log('browser', function(err,logs) {
+                if (err) console.log(err);
+                console.log(cfg.browserName,cfg.platform,'log');
+
+                var logfile = baseDir+'/'+browser+'_'+platform+'.log'
+                logs = logs || [];
+                fs.writeFile(logfile,logs.join('\n'), function(err) {
+                  if (err) console.log(err);
+
+                  browserDriver.quit();
+                });
+              });
+
+            });
+          } else {
+            var scrollTop = 0;
+
+            // loop generates the images. Firefox and Internet Explorer will take
+            // a screenshot of the entire webpage, but Opera, Safari, and Chrome
+            // do not. For those browsers we scroll through the page and take
+            // incremental screenshots.
+            (function loop() {
+              var index = (scrollTop/viewportHeight) + 1;
+
+              // Use `window.scrollTo` because thats what jQuery does
+              // https://github.com/jquery/jquery/blob/1.12.3/src/offset.js#L186
+              // `window.scrollTo` was used instead of jQuery because jQuery was
+              // causing a stackoverflow in Safari.
+              browserDriver.safeEval('window.scrollTo(0,'+scrollTop+');', function(err) {
+                if (err) console.log(JSON.stringify(err));
+                console.log(cfg.browserName,cfg.platform,'safeEval 1');
+
+                // saves file in the file `piecesDir/browser_version_platform/#.png`
+                var filename = piecesDir+'/'+index+'.png';
+                browserDriver.saveScreenshot(filename, function(err) {
+                  if (err) console.log(err);
+                  console.log(cfg.browserName,cfg.platform,'saveScreenshot');
+
+                  scrollTop += viewportHeight;
+                  if (scrollTop + viewportHeight > scrollHeight) {
+                    browserDriver.getWindowSize(function(err,size) {
+                      if (err) console.log(err);
+                      console.log(cfg.browserName,cfg.platform,'getWindowSize');
+                      // account for the viewport offset
+                      var extra = size.height - viewportHeight;
+                      browserDriver.setWindowSize(size.width, (scrollHeight-scrollTop)+extra, function(err) {
+                        if (err) console.log(err);
+                        console.log(cfg.browserName,cfg.platform,'setWindowSize');
+
+                        browserDriver.safeEval('window.scrollTo(0,'+scrollHeight+');', function(err) {
+                          if (err) console.log(JSON.stringify(err));
+                          console.log(cfg.browserName,cfg.platform,'safeEval 2');
+
+                          index++;
+                          var filename = piecesDir+'/'+index+'.png';
+                          browserDriver.saveScreenshot(filename, function(err) {
+                            if (err) console.log(err);
+                            console.log(cfg.browserName,cfg.platform,'saveScreenshot Final');
+
+                            browserDriver.log('browser', function(err,logs) {
+                              if (err) console.log(err);
+                              console.log(cfg.browserName,cfg.platform,'log');
+
+                              var logfile = baseDir+'/'+browser+'_'+platform+'.log'
+                              logs = logs || [];
+                              fs.writeFile(logfile,logs.join('\n'), function(err) {
+                                if (err) console.log(err);
+                                console.log(cfg.browserName,cfg.platform,'writeFile');
+
+                                browserDriver.quit();
+                              });
+                            });
+                          });
+                        });
+
+                      });
+                    });
+                  } else {
+                    loop();
+                  }
+                });
+              });
+            })();
+          }
+        });
+      });
+    });
+  });
+});
diff --git a/script/test_server.js b/script/test_server.js
index a551bae..17a7420 100644
--- a/script/test_server.js
+++ b/script/test_server.js
@@ -35,6 +35,8 @@
         }
       }
       else {
+        var ext = filepath.match(/\.[^.]+$/);
+        if (ext) res.setHeader('Content-Type', 'text/' + ext[0].slice(1));
         res.end(data);
       }
 
diff --git a/src/commands/math/basicSymbols.js b/src/commands/math/basicSymbols.js
index a8578ae..8c80b32 100644
--- a/src/commands/math/basicSymbols.js
+++ b/src/commands/math/basicSymbols.js
@@ -421,12 +421,28 @@
   _.init = VanillaSymbol.prototype.init;
 
   _.contactWeld = _.siblingCreated = _.siblingDeleted = function(opts, dir) {
+    function determineOpClassType(node) {
+      if (node[L]) {
+        // If the left sibling is a binary operator or a separator (comma, semicolon, colon)
+        // or an open bracket (open parenthesis, open square bracket)
+        // consider the operator to be unary
+        if (node[L] instanceof BinaryOperator || /^[,;:\(\[]$/.test(node[L].ctrlSeq)) {
+          return '';
+        }
+      } else if (node.parent && node.parent.parent && node.parent.parent.isStyleBlock()) {
+        //if we are in a style block at the leftmost edge, determine unary/binary based on
+        //the style block
+        //this allows style blocks to be transparent for unary/binary purposes
+        return determineOpClassType(node.parent.parent);
+      } else {
+        return '';
+      }
+
+      return 'mq-binary-operator';
+    };
+    
     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 || /^[,;:\(\[]$/.test(this[L].ctrlSeq) ? '' : 'mq-binary-operator');
+    this.jQ[0].className = determineOpClassType(this);
     return this;
   };
 });
diff --git a/src/commands/math/commands.js b/src/commands/math/commands.js
index 1538c51..fdd3cf2 100644
--- a/src/commands/math/commands.js
+++ b/src/commands/math/commands.js
@@ -114,6 +114,9 @@
       })
     ;
   };
+  _.isStyleBlock = function() {
+    return true;
+  };
 });
 
 // Very similar to the \textcolor command, but will add the given CSS class.
@@ -133,6 +136,9 @@
       })
     ;
   };
+  _.isStyleBlock = function() {
+    return true;
+  };
 });
 
 var SupSub = P(MathCommand, function(_, super_) {
@@ -474,15 +480,15 @@
   };
 });
 
-var Vec = LatexCmds.vec = P(MathCommand, function(_, super_) {
-  _.ctrlSeq = '\\vec';
+var Hat = LatexCmds.hat = P(MathCommand, function(_, super_) {
+  _.ctrlSeq = '\\hat';
   _.htmlTemplate =
       '<span class="mq-non-leaf">'
-    +   '<span class="mq-vector-prefix">&rarr;</span>'
-    +   '<span class="mq-vector-stem">&0</span>'
+    +   '<span class="mq-hat-prefix">^</span>'
+    +   '<span class="mq-hat-stem">&0</span>'
     + '</span>'
   ;
-  _.textTemplate = ['vec(', ')'];
+  _.textTemplate = ['hat(', ')'];
 });
 
 var NthRoot =
@@ -500,6 +506,21 @@
   };
 });
 
+var DiacriticAbove = P(MathCommand, function(_, super_) {
+  _.init = function(ctrlSeq, symbol, textTemplate) {
+    var htmlTemplate =
+      '<span class="mq-non-leaf">'
+      +   '<span class="mq-diacritic-above">'+symbol+'</span>'
+      +   '<span class="mq-diacritic-stem">&0</span>'
+      + '</span>'
+    ;
+
+    super_.init.call(this, ctrlSeq, htmlTemplate, textTemplate);
+  };
+});
+LatexCmds.vec = bind(DiacriticAbove, '\\vec', '&rarr;', ['vec(', ')']);
+LatexCmds.tilde = bind(DiacriticAbove, '\\tilde', '~', ['tilde(', ')']);
+
 function DelimsMixin(_, super_) {
   _.jQadd = function() {
     super_.jQadd.apply(this, arguments);
@@ -573,8 +594,8 @@
         Fragment(cursor[side], cursor.parent.ends[side], -side) // me and ghost outside
           .disown().withDirAdopt(-side, brack.parent, brack, brack[side])
           .jQ.insDirOf(side, brack.jQ);
-        brack.bubble('reflow');
       }
+      brack.bubble('reflow');
     }
     else {
       brack = this, side = brack.side;
@@ -673,7 +694,9 @@
   '&rang;': '&lang;',
   '\\langle ': '\\rangle ',
   '\\rangle ': '\\langle ',
-  '|': '|'
+  '|': '|',
+  '\\lVert ' : '\\rVert ',
+  '\\rVert ' : '\\lVert ',
 };
 
 function bindCharBracketPair(open, ctrlSeq) {
@@ -687,6 +710,8 @@
 LatexCmds.langle = bind(Bracket, L, '&lang;', '&rang;', '\\langle ', '\\rangle ');
 LatexCmds.rangle = bind(Bracket, R, '&lang;', '&rang;', '\\langle ', '\\rangle ');
 CharCmds['|'] = bind(Bracket, L, '|', '|', '|', '|');
+LatexCmds.lVert = bind(Bracket, L, '&#8741;', '&#8741;', '\\lVert ', '\\rVert ');
+LatexCmds.rVert = bind(Bracket, R, '&#8741;', '&#8741;', '\\lVert ', '\\rVert ');
 
 LatexCmds.left = P(MathCommand, function(_) {
   _.parser = function() {
@@ -695,13 +720,17 @@
     var succeed = Parser.succeed;
     var optWhitespace = Parser.optWhitespace;
 
-    return optWhitespace.then(regex(/^(?:[([|]|\\\{)/))
-      .then(function(ctrlSeq) { // TODO: \langle, \rangle
+    return optWhitespace.then(regex(/^(?:[([|]|\\\{|\\langle\b|\\lVert\b)/))
+      .then(function(ctrlSeq) {
         var open = (ctrlSeq.charAt(0) === '\\' ? ctrlSeq.slice(1) : ctrlSeq);
+	if (ctrlSeq=="\\langle") { open = '&lang;'; ctrlSeq = ctrlSeq + ' '; }
+	if (ctrlSeq=="\\lVert") { open = '&#8741;'; ctrlSeq = ctrlSeq + ' '; }
         return latexMathParser.then(function (block) {
           return string('\\right').skip(optWhitespace)
-            .then(regex(/^(?:[\])|]|\\\})/)).map(function(end) {
+            .then(regex(/^(?:[\])|]|\\\}|\\rangle\b|\\rVert\b)/)).map(function(end) {
               var close = (end.charAt(0) === '\\' ? end.slice(1) : end);
+	      if (end=="\\rangle") { close = '&rang;'; end = end + ' '; }
+	      if (end=="\\rVert") { close = '&#8741;'; end = end + ' '; }
               var cmd = Bracket(0, open, close, ctrlSeq, end);
               cmd.blocks = [ block ];
               block.adopt(cmd, 0, 0);
@@ -791,7 +820,7 @@
     return this;
   };
   _.parser = function() {
-    var self = this;
+    var self = this,
       string = Parser.string, regex = Parser.regex, succeed = Parser.succeed;
     return string('{').then(regex(/^[a-z][a-z0-9]*/i)).skip(string('}'))
       .then(function(name) {
diff --git a/src/css/math.less b/src/css/math.less
index aae409d..389058c 100644
--- a/src/css/math.less
+++ b/src/css/math.less
@@ -294,15 +294,29 @@
     padding-top: 1px;
   }
 
-  .mq-vector-prefix {
+  .mq-diacritic-above {
     display: block;
     text-align: center;
-    line-height: .25em;
-    margin-bottom: -.1em;
-    font-size: 0.75em;
+    line-height: .4em;
   }
 
-  .mq-vector-stem {
+  .mq-diacritic-stem {
+    display: block;
+    text-align: center;
+  }
+  
+  .mq-hat-prefix {
+    display: block;
+    text-align: center;
+    line-height: .95em;
+    margin-bottom: -.7em;
+    transform: scaleX(1.5);
+    -moz-transform: scaleX(1.5);
+    -o-transform: scaleX(1.5);
+    -webkit-transform: scaleX(1.5);
+  }
+
+  .mq-hat-stem {
     display: block;
   }
 
diff --git a/src/css/textarea.less b/src/css/textarea.less
index 1484fa7..4da9bdb 100644
--- a/src/css/textarea.less
+++ b/src/css/textarea.less
@@ -20,5 +20,10 @@
 
     width: 1px; // don't "stick out" invisibly from a math field,
     height: 1px; // can affect ancestor's .scroll{Width,Height}
+
+    // Needed to fix a Safari 10 bug where box-sizing: border-box is
+    // preventing text from being copied.
+    // https://github.com/mathquill/mathquill/issues/686
+    box-sizing: content-box;
   }
 }
diff --git a/src/tree.js b/src/tree.js
index 649c517..335dafa 100644
--- a/src/tree.js
+++ b/src/tree.js
@@ -156,6 +156,10 @@
   _.isEmpty = function() {
     return this.ends[L] === 0 && this.ends[R] === 0;
   };
+  
+  _.isStyleBlock = function() {
+    return false;
+  };
 
   _.children = function() {
     return Fragment(this.ends[L], this.ends[R]);
diff --git a/test/unit.html b/test/unit.html
index 04fb3b6..601820c 100644
--- a/test/unit.html
+++ b/test/unit.html
@@ -78,7 +78,7 @@
 
         runner.on('end', function() {
           setTimeout(function() {
-            $.post(post_xunit_to, xunit, function() {
+            $.post(post_xunit_to, xunit).always(function() {
               window.mochaResults = runner.stats;
               window.mochaResults.reports = failedTests;
             });
diff --git a/test/unit/css.test.js b/test/unit/css.test.js
index 639cdf9..56e12d2 100644
--- a/test/unit/css.test.js
+++ b/test/unit/css.test.js
@@ -82,6 +82,37 @@
     $(mq.el()).remove();
   });
 
+  test('proper unary/binary within style block', function () {
+    var mq = MQ.MathField($('<span></span>').appendTo('#mock')[0]);
+    mq.latex('\\class{dummy}{-}2\\class{dummy}{+}4');
+    var spans = $(mq.el()).find('.mq-root-block').find('span');
+    assert.equal(spans.length, 6, '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, '\\class{dummy}{-}');
+    assertBinaryOperator(4, '\\class{dummy}{-}2\\class{dummy}{+}');
+
+    mq.latex('\\textcolor{red}{-}2\\textcolor{green}{+}4');
+    spans = $(mq.el()).find('.mq-root-block').find('span');
+    assert.equal(spans.length, 6, 'PlusMinus expression parsed incorrectly');
+
+    assertUnaryOperator(1, '\\textcolor{red}{-}');
+    assertBinaryOperator(4, '\\textcolor{red}{-}2\\textcolor{green}{+}');
+
+    //test recursive depths
+    mq.latex('\\textcolor{red}{\\class{dummy}{-}}2\\textcolor{green}{\\class{dummy}{+}}4');
+    spans = $(mq.el()).find('.mq-root-block').find('span');
+    assert.equal(spans.length, 8, 'PlusMinus expression parsed incorrectly');
+
+    assertUnaryOperator(2, '\\textcolor{red}{\\class{dummy}{-}}');
+    assertBinaryOperator(6, '\\textcolor{red}{\\class{dummy}{-}}2\\textcolor{green}{\\class{dummy}{+}}');
+
+    $(mq.el()).remove();
+  });
+
   test('operator name spacing e.g. sin x', function() {
     var mq = MathQuill.MathField($('<span></span>').appendTo(mock)[0]);
 
diff --git a/test/unit/focusBlur.test.js b/test/unit/focusBlur.test.js
index 27180ae..20b4fbd 100644
--- a/test/unit/focusBlur.test.js
+++ b/test/unit/focusBlur.test.js
@@ -80,7 +80,7 @@
           $(mq.el()).remove();
           done();
         });
-      }, 10);
+      }, 100);
     });
   });
 });
diff --git a/test/unit/latex.test.js b/test/unit/latex.test.js
index 4cf59fa..ab6d925 100644
--- a/test/unit/latex.test.js
+++ b/test/unit/latex.test.js
@@ -85,6 +85,24 @@
     assert.equal(tree.join('latex'), '\\left(123\\right)');
   });
 
+  test('langle/rangle (issue #508)', function() {
+    var tree = latexMathParser.parse('\\left\\langle 123\\right\\rangle)');
+
+    assert.ok(tree.ends[L] instanceof Bracket);
+    var contents = tree.ends[L].ends[L].join('latex');
+    assert.equal(contents, '123');
+    assert.equal(tree.join('latex'), '\\left\\langle 123\\right\\rangle )');
+  });
+
+  test('lVert/rVert', function() {
+    var tree = latexMathParser.parse('\\left\\lVert 123\\right\\rVert)');
+
+    assert.ok(tree.ends[L] instanceof Bracket);
+    var contents = tree.ends[L].ends[L].join('latex');
+    assert.equal(contents, '123');
+    assert.equal(tree.join('latex'), '\\left\\lVert 123\\right\\rVert )');
+  });
+
   test('parens with whitespace', function() {
     assertParsesLatex('\\left ( 123 \\right ) ', '\\left(123\\right)');
   });
@@ -308,5 +326,7 @@
     testCantParse('unmatched close brace', '}', ' 1 + 2 } ', '1 - {2 + 3} }', '\\sqrt{ x }} + \\sqrt{y}');
     testCantParse('unmatched open brace', '{', '1 * { 2 + 3', '\\frac{ \\sqrt x }{{ \\sqrt y}');
     testCantParse('unmatched \\left/\\right', '\\left ( 1 + 2 )', ' [ 1, 2 \\right ]');
+    testCantParse('langlerfish/ranglerfish (checking for confusion with langle/rangle)',
+		    '\\left\\langlerfish 123\\right\\ranglerfish)');
   });
 });
diff --git a/test/unit/publicapi.test.js b/test/unit/publicapi.test.js
index 55645b6..4791491 100644
--- a/test/unit/publicapi.test.js
+++ b/test/unit/publicapi.test.js
@@ -304,6 +304,23 @@
       });
     }
   });
+  
+  suite('edit handler', function() {
+    test('fires when closing a bracket expression', function() {
+      var count = 0;
+      var mq = MQ.MathField($('<span>').appendTo('#mock')[0], {
+        handlers: {
+          edit: function() {
+            count += 1;
+          }
+        }
+      });
+      mq.typedText('(3, 4');
+      var countBeforeClosingBracket = count;
+      mq.typedText(']');
+      assert.equal(count, countBeforeClosingBracket + 1);
+    });
+  });
 
   suite('.cmd(...)', function() {
     var mq;
diff --git a/test/visual.html b/test/visual.html
index f066a7b..a5918b5 100644
--- a/test/visual.html
+++ b/test/visual.html
@@ -161,8 +161,6 @@
 

 <p id="selection-tests"><span class="mathquill-text-field different-bgcolor">lolwut $a^2 + b^2 = c^2$. $\sqrt{ \left( \frac{1}{2} \right) }$.  Also, awesomesauce: $\int_0^1 \sin x dx.</span>

 

-<p>Time taken to Select All (should be &lt;50ms): <span id="selection-performance"></span>

-

 <p>Even in IE&lt;9, the background color of the parens and square root radical should be the background color of the selection.

 

 <h3>Dynamic mathquill-ification</h3>

@@ -211,6 +209,7 @@
 <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">\vec x + \tilde x + \vec A + \tilde A + \vec{abcd} + \tilde{abcd}</span><td><span>\vec x + \tilde x + \vec A + \tilde A + \vec{abcd} + \tilde{abcd}</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>

@@ -279,7 +278,8 @@
 

 </div>

 <script type="text/javascript">

-window.onerror = function() {

+window.onerror = function(err) {

+  console.log(err); // to show up in Selenium WebDriver logs

   $('html').css('background', 'red');

 };

 </script>

@@ -329,7 +329,7 @@
     $('#selection-tests .mathquill-text-field').each(function() {

       var start = +new Date;

       $('textarea', this).focus().trigger({type: 'keydown', ctrlKey: true, which: 65});

-      $('#selection-performance').html(new Date - start);

+      console.log('Time taken to Select All (should be &lt;50ms):',new Date - start);

     });

   });

 };