function moveUpDown(self, dir) {
    if (self[R][dir]) self.insAtLeftEnd(self[R][dir]);
    else if (self[L][dir]) self.insAtRightEnd(self[L][dir]);
    else {
      var ancestorBlock = self.parent;
      do {
        var prop = ancestorBlock[dir];
        if (prop) {
          if (typeof prop === 'function') prop = ancestorBlock[dir](self);
          if (prop === false || prop instanceof MathBlock) {
            self.upDownCache[ancestorBlock.id] = Point(self.parent, self[L], self[R]);

            if (prop instanceof MathBlock) {
              var cached = self.upDownCache[prop.id];

              if (cached) {
                if (cached[R]) {
                  self.insLeftOf(cached[R]);
                } else {
                  self.insAtRightEnd(cached.parent);
                }
              } else {
                var pageX = offset(self).left;
                self.insAtRightEnd(prop);
                self.seekHoriz(pageX, prop);
              }
            }
            break;
          }
        }
        ancestorBlock = ancestorBlock.parent.parent;
      } while (ancestorBlock);
    }

    return self.clearSelection().show();
  }
moveUp and moveDown have almost identical algorithms:
- first check left and right, if so insAtLeft/RightEnd of them
- else check the parent's 'up'/'down' property - if it's a function,
  call it with the cursor as the sole argument and use the return value.

  Given undefined, will bubble up to the next ancestor block.
  Given false, will stop bubbling.
  Given a MathBlock,
    + moveUp will insAtRightEnd of it
    + moveDown will insAtLeftEnd of it