Package.describe({
  summary: "Run tests interactively in the browser",
  version: '1.2.0',
  documentation: null
});

Package.onUse(function (api) {
  api.use('ecmascript');
  // XXX this should go away, and there should be a clean interface
  // that tinytest and the driver both implement?
  api.use('tinytest');
  api.use('bootstrap@1.0.1');
  api.use('underscore');

  api.use('session');
  api.use('reload');
  api.use('jquery@1.11.1');

  api.use(['webapp', 'blaze@2.1.8', 'templating@1.2.13', 'spacebars@1.0.12',
           'ddp', 'tracker'], 'client');

  api.addFiles('diff_match_patch_uncompressed.js', 'client');

  api.addFiles([
    'driver.html',
    'driver.js',
    'driver.css'
  ], "client");

  api.use("random", "server");
  api.mainModule("server.js", "server");

  api.export('runTests');
});


keep keep keep keep replace replace replace keep keep


keep keep replace replace replace keep keep keep


import { pathJoin, getDevBundle } from '../fs/files.js';
import { installNpmModule, moduleDoesResolve } from '../isobuild/meteor-npm.js';

export function ensureDependencies(deps) {
  // Check if each of the requested dependencies resolves, if not
  // mark them for installation.
  const needToInstall = Object.create(null);
  Object.keys(deps).forEach(dep => {
    if (!moduleDoesResolve(dep)) {
      const versionToInstall = deps[dep];
      needToInstall[dep] = versionToInstall;
    }
  });

  const devBundleLib = pathJoin(getDevBundle(), 'lib');

  // Install each of the requested modules.
  Object.keys(needToInstall)
    .forEach(dep => installNpmModule(dep, needToInstall[dep], devBundleLib));
}


Package.describe({
  summary: "Reactive dictionary",
  version: '1.3.0'
});

Package.onUse(function (api) {
  api.use(['tracker', 'ejson', 'ecmascript']);
  // If we are loading mongo-livedata, let you store ObjectIDs in it.
  api.use(['mongo', 'reload'], { weak: true });
  api.mainModule('migration.js');
  api.export('ReactiveDict');
});

Package.onTest(function (api) {
  api.use('tinytest');
  api.use('reactive-dict');
  api.use('tracker');
  api.use('reload');
  api.addFiles('reactive-dict-tests.js');
});


var assert = require("assert");
var files = require('../fs/files.js');
var _ = require("underscore");

// This class encapsulates a structured specification of files and
// directories that should be stripped from the node_modules directories
// of Meteor packages during `meteor build`, as requested by calling
// `Npm.discard` in package.js files.
function NpmDiscards() {
  assert.ok(this instanceof NpmDiscards);
  this.discards = {};
}

var NDp = NpmDiscards.prototype;

// Update the current specification of discarded files with additional
// patterns that should be discarded. See the comment in package-source.js
// about `Npm.strip` for an explanation of what should be passed for the
// `discards` parameter.
NDp.merge = function(discards) {
  merge(this.discards, discards);
};

function merge(into, from) {
  _.each(from, function(fromValue, packageName) {
    var intoValue = _.has(into, packageName) && into[packageName];
    if (_.isString(fromValue) ||
        _.isRegExp(fromValue)) {
      if (intoValue) {
        intoValue.push(fromValue);
      } else {
        into[packageName] = [fromValue];
      }
    } else if (_.isArray(fromValue)) {
      if (intoValue) {
        intoValue.push.apply(intoValue, fromValue);
      } else {
        // Make a defensive copy of any arrays passed to `Npm.strip`.
        into[packageName] = fromValue.slice(0);
      }
    }
  });
}

NDp.shouldDiscard = function shouldDiscard(candidatePath, isDirectory) {
  if (typeof isDirectory === "undefined") {
    isDirectory = files.lstat(candidatePath).isDirectory();
  }

  for (var currentPath = candidatePath, parentPath;
       (parentPath = files.pathDirname(currentPath)) !== currentPath;
       currentPath = parentPath) {
    if (files.pathBasename(parentPath) === "node_modules") {
      var packageName = files.pathBasename(currentPath);

      if (_.has(this.discards, packageName)) {
        var relPath = files.pathRelative(currentPath, candidatePath);

        if (isDirectory) {
          relPath = files.pathJoin(relPath, files.pathSep);
        }

        return this.discards[packageName].some(function(pattern) {
          return matches(pattern, relPath);
        });
      }

      // Stop at the first ancestor node_modules directory we find.
      break;
    }
  }

  return false;
};

// TODO Improve this. For example we don't currently support wildcard
// string patterns (just use a RegExp if you need that flexibility).
function matches(pattern, relPath) {
  if (_.isRegExp(pattern)) {
    return relPath.match(pattern);
  }

  assert.ok(_.isString(pattern));

  if (pattern.charAt(0) === files.pathSep) {
    return relPath.indexOf(pattern.slice(1)) === 0;
  }

  return relPath.indexOf(pattern) !== -1;
}

module.exports = NpmDiscards;


keep keep keep replace keep keep keep keep keep

// This file contains a package.json for the dependencies of the command-line
// tool.

// We put this in a JS file so that it can contain comments. It is processed
// into a package.json file by generate-dev-bundle.sh.

var packageJson = {
  name: "meteor-dev-bundle-tool",
  private: true,
  dependencies: {
    // Explicit dependency because we are replacing it with a bundled version
    // and we want to make sure there are no dependencies on a higher version
    npm: "6.9.0",
    pacote: "https://github.com/meteor/pacote/tarball/c5043daa1b768594e01d76275e3854fc19f038f9",
    "node-gyp": "3.7.0",
    "node-pre-gyp": "0.10.3",
    "meteor-babel": "7.4.12",
    // Keep the versions of these packages consistent with the versions
    // found in dev-bundle-server-package.js.
    "meteor-promise": "0.8.7",
    reify: "0.20.4",
    fibers: "3.1.1",
    // So that Babel can emit require("@babel/runtime/helpers/...") calls.
    "@babel/runtime": "7.4.4",
    // For backwards compatibility with isopackets that still depend on
    // babel-runtime rather than @babel/runtime.
    "babel-runtime": "7.0.0-beta.3",
    // Not yet upgrading Underscore from 1.5.2 to 1.7.0 (which should be done
    // in the package too) because we should consider using lodash instead
    // (and there are backwards-incompatible changes either way).
    underscore: "1.5.2",
    "source-map-support": "https://github.com/meteor/node-source-map-support/tarball/1912478769d76e5df4c365e147f25896aee6375e",
    semver: "5.4.1",
    request: "2.88.0",
    uuid: "3.3.2",
    fstream: "https://github.com/meteor/fstream/tarball/cf4ea6c175355cec7bee38311e170d08c4078a5d",
    tar: "2.2.1",
    kexec: "3.0.0",
    "source-map": "0.5.7",
    chalk: "0.5.1",
    sqlite3: "3.1.8",
    netroute: "1.0.2",
    "http-proxy": "1.16.2",
    "is-reachable": "3.1.0",
    "wordwrap": "0.0.2",
    "moment": "2.20.1",
    "rimraf": "2.6.2",
    "glob": "7.1.2",
    ignore: "3.3.7",
    // XXX: When we update this, see if it fixes this Github issue:
    // https://github.com/jgm/CommonMark/issues/276 . If it does, remove the
    // workaround from the tool.
    "commonmark": "0.15.0",
    escope: "3.6.0",
    split2: "2.2.0",
    multipipe: "2.0.1",
    pathwatcher: "8.0.2",
    // The @wry/context package version must be compatible with the
    // version constraint imposed by optimism/package.json.
    optimism: "0.9.5",
    "@wry/context": "0.4.0",
    'lru-cache': '4.1.3'
  }
};

if (process.platform === 'win32') {
  // Remove dependencies that do not work on Windows
  delete packageJson.dependencies.netroute;
  delete packageJson.dependencies.kexec;
}

process.stdout.write(JSON.stringify(packageJson, null, 2) + '\n');


This directory and the files immediately inside it are automatically generated
when you change this package's NPM dependencies. Commit the files in this
directory (npm-shrinkwrap.json, .gitignore, and this README) to source control
so that others run the same versions of sub-dependencies.

You should NOT check in the node_modules directory that Meteor automatically
creates; if you are using git, the .gitignore file tells git to ignore it.


# observe-sequence
[Source code of released version](https://github.com/meteor/meteor/tree/master/packages/observe-sequence) | [Source code of development version](https://github.com/meteor/meteor/tree/devel/packages/observe-sequence)
***

This is an internal Meteor package.


// Keep the versions of these packages consistent with the versions

// found in dev-bundle-server-package.js.

keep keep keep keep replace keep keep keep


// Constructor of Heap
// - comparator - Function - given two items returns a number
// - options:
//   - initData - Array - Optional - the initial data in a format:
//        Object:
//          - id - String - unique id of the item
//          - value - Any - the data value
//      each value is retained
//   - IdMap - Constructor - Optional - custom IdMap class to store id->index
//       mappings internally. Standard IdMap is used by default.
export class MaxHeap {
  constructor(comparator, options = {}) {
    if (typeof comparator !== 'function') {
      throw new Error('Passed comparator is invalid, should be a comparison function');
    }

    // a C-style comparator that is given two values and returns a number,
    // negative if the first value is less than the second, positive if the second
    // value is greater than the first and zero if they are equal.
    this._comparator = comparator;

    if (! options.IdMap) {
      options.IdMap = IdMap;
    }

    // _heapIdx maps an id to an index in the Heap array the corresponding value
    // is located on.
    this._heapIdx = new options.IdMap;

    // The Heap data-structure implemented as a 0-based contiguous array where
    // every item on index idx is a node in a complete binary tree. Every node can
    // have children on indexes idx*2+1 and idx*2+2, except for the leaves. Every
    // node has a parent on index (idx-1)/2;
    this._heap = [];

    // If the initial array is passed, we can build the heap in linear time
    // complexity (O(N)) compared to linearithmic time complexity (O(nlogn)) if
    // we push elements one by one.
    if (Array.isArray(options.initData)) {
      this._initFromData(options.initData);
    }
  }

  // Builds a new heap in-place in linear time based on passed data
  _initFromData(data) {
    this._heap = data.map(({ id, value }) => ({ id, value }));

    data.forEach(({ id }, i) => this._heapIdx.set(id, i));

    if (! data.length) {
      return;
    }

    // start from the first non-leaf - the parent of the last leaf
    for (let i = parentIdx(data.length - 1); i >= 0; i--) {
      this._downHeap(i);
    }
  }

  _downHeap(idx) {
    while (leftChildIdx(idx) < this.size()) {
      const left = leftChildIdx(idx);
      const right = rightChildIdx(idx);
      let largest = idx;

      if (left < this.size()) {
        largest = this._maxIndex(largest, left);
      }

      if (right < this.size()) {
        largest = this._maxIndex(largest, right);
      }

      if (largest === idx) {
        break;
      }

      this._swap(largest, idx);
      idx = largest;
    }
  }

  _upHeap(idx) {
    while (idx > 0) {
      const parent = parentIdx(idx);
      if (this._maxIndex(parent, idx) === idx) {
        this._swap(parent, idx)
        idx = parent;
      } else {
        break;
      }
    }
  }

  _maxIndex(idxA, idxB) {
    const valueA = this._get(idxA);
    const valueB = this._get(idxB);
    return this._comparator(valueA, valueB) >= 0 ? idxA : idxB;
  }

  // Internal: gets raw data object placed on idxth place in heap
  _get(idx) {
    return this._heap[idx].value;
  }

  _swap(idxA, idxB) {
    const recA = this._heap[idxA];
    const recB = this._heap[idxB];

    this._heapIdx.set(recA.id, idxB);
    this._heapIdx.set(recB.id, idxA);

    this._heap[idxA] = recB;
    this._heap[idxB] = recA;
  }

  get(id) {
    return this.has(id) ?
      this._get(this._heapIdx.get(id)) :
      null;
  }

  set(id, value) {
    if (this.has(id)) {
      if (this.get(id) === value) {
        return;
      }

      const idx = this._heapIdx.get(id);
      this._heap[idx].value = value;

      // Fix the new value's position
      // Either bubble new value up if it is greater than its parent
      this._upHeap(idx);
      // or bubble it down if it is smaller than one of its children
      this._downHeap(idx);
    } else {
      this._heapIdx.set(id, this._heap.length);
      this._heap.push({ id, value });
      this._upHeap(this._heap.length - 1);
    }
  }

  remove(id) {
    if (this.has(id)) {
      const last = this._heap.length - 1;
      const idx = this._heapIdx.get(id);

      if (idx !== last) {
        this._swap(idx, last);
        this._heap.pop();
        this._heapIdx.remove(id);

        // Fix the swapped value's position
        this._upHeap(idx);
        this._downHeap(idx);
      } else {
        this._heap.pop();
        this._heapIdx.remove(id);
      }
    }
  }

  has(id) {
    return this._heapIdx.has(id);
  }

  empty() {
    return !this.size();
  }

  clear() {
    this._heap = [];
    this._heapIdx.clear();
  }

  // iterate over values in no particular order
  forEach(iterator) {
    this._heap.forEach(obj => iterator(obj.value, obj.id));
  }

  size() {
    return this._heap.length;
  }

  setDefault(id, def) {
    if (this.has(id)) {
      return this.get(id);
    }

    this.set(id, def);
    return def;
  }

  clone() {
    const clone = new MaxHeap(this._comparator, this._heap);
    return clone;
  }

  maxElementId() {
    return this.size() ? this._heap[0].id : null;
  }

  _selfCheck() {
    for (let i = 1; i < this._heap.length; i++) {
      if (this._maxIndex(parentIdx(i), i) !== parentIdx(i)) {
          throw new Error(`An item with id ${this._heap[i].id}` +
                          " has a parent younger than it: " +
                          this._heap[parentIdx(i)].id);
      }
    }
  }
}

const leftChildIdx = i => i * 2 + 1;
const rightChildIdx = i => i * 2 + 2;
const parentIdx = i => (i - 1) >> 1;


Package.describe({
  name: 'coffeescript',
  summary: 'JavaScript dialect with fewer braces and semicolons',
  // This package version should track the version of the `coffeescript-compiler`
  // package, because people will likely only have this one added to their apps;
  // so bumping the version of this package will be how they get newer versions
  // of `coffeescript-compiler`. If you change this, make sure to also update
  // ../coffeescript-compiler/package.js to match.
  version: '2.3.2_1'
});

Package.registerBuildPlugin({
  name: 'compile-coffeescript',
  use: ['caching-compiler@1.1.12', 'ecmascript@0.11.1', 'coffeescript-compiler@2.3.2_1'],
  sources: ['compile-coffeescript.js'],
  npmDependencies: {
    // A breaking change was introduced in @babel/runtime@7.0.0-beta.56
    // with the removal of the @babel/runtime/helpers/builtin directory.
    // Since the compile-coffeescript plugin is bundled and published with
    // a specific version of babel-compiler and babel-runtime, it also
    // needs to have a reliable version of the @babel/runtime npm package,
    // rather than delegating to the one installed in the application's
    // node_modules directory, so the coffeescript package can work in
    // Meteor 1.7.1 apps as well as 1.7.0.x and earlier.
    '@babel/runtime': '7.1.2'
  }
});

Package.onUse(function (api) {
  api.use('isobuild:compiler-plugin@1.0.0');

  // Because the CoffeeScript plugin now calls
  // BabelCompiler.prototype.processOneFileForTarget for any ES2015+
  // JavaScript or JavaScript enclosed by backticks, it must provide the
  // same runtime environment that the 'ecmascript' package provides.
  // The following api.imply calls should match those in ../../ecmascript/package.js,
  // except that coffeescript does not api.imply('modules').
  api.imply('ecmascript-runtime@0.7.0');
  api.imply('babel-runtime@1.2.4');
  api.imply('promise@0.11.1');
  api.imply('dynamic-import@0.4.1');
});

Package.onTest(function (api) {
  api.use(['coffeescript', 'tinytest', 'modern-browsers']);
  api.use(['coffeescript-test-helper', 'ecmascript'], ['client', 'server']); // Need ecmascript to compile tests/es2015_module.js
  api.addFiles('tests/bare_test_setup.coffee', ['client'], {bare: true});
  api.addFiles('tests/bare_tests.js', ['client']);
  api.addFiles([
    'tests/coffeescript_test_setup.js',
    'tests/coffeescript_tests.coffee',
    'tests/coffeescript_strict_tests.coffee',
    'tests/coffeescript_module.coffee',
    'tests/es2015_module.js',
    'tests/litcoffeescript_tests.litcoffee',
    'tests/litcoffeescript_tests.coffee.md',
    'tests/coffeescript_tests.js'
  ], ['client', 'server']);
  api.addFiles('tests/modern_browsers.coffee', ['server']);
});


Plugin.registerCompiler({
  extensions: ['myext']
}, function () {
  return { processFilesForTarget: function () {} };
});


// So that Babel can emit require("@babel/runtime/helpers/...") calls.

// For backwards compatibility with isopackets that still depend on

keep keep keep replace keep keep keep keep


var Future = Npm.require('fibers/future');

var PHASE = {
  QUERYING: "QUERYING",
  FETCHING: "FETCHING",
  STEADY: "STEADY"
};

// Exception thrown by _needToPollQuery which unrolls the stack up to the
// enclosing call to finishIfNeedToPollQuery.
var SwitchedToQuery = function () {};
var finishIfNeedToPollQuery = function (f) {
  return function () {
    try {
      f.apply(this, arguments);
    } catch (e) {
      if (!(e instanceof SwitchedToQuery))
        throw e;
    }
  };
};

var currentId = 0;

// OplogObserveDriver is an alternative to PollingObserveDriver which follows
// the Mongo operation log instead of just re-polling the query. It obeys the
// same simple interface: constructing it starts sending observeChanges
// callbacks (and a ready() invocation) to the ObserveMultiplexer, and you stop
// it by calling the stop() method.
OplogObserveDriver = function (options) {
  var self = this;
  self._usesOplog = true;  // tests look at this

  self._id = currentId;
  currentId++;

  self._cursorDescription = options.cursorDescription;
  self._mongoHandle = options.mongoHandle;
  self._multiplexer = options.multiplexer;

  if (options.ordered) {
    throw Error("OplogObserveDriver only supports unordered observeChanges");
  }

  var sorter = options.sorter;
  // We don't support $near and other geo-queries so it's OK to initialize the
  // comparator only once in the constructor.
  var comparator = sorter && sorter.getComparator();

  if (options.cursorDescription.options.limit) {
    // There are several properties ordered driver implements:
    // - _limit is a positive number
    // - _comparator is a function-comparator by which the query is ordered
    // - _unpublishedBuffer is non-null Min/Max Heap,
    //                      the empty buffer in STEADY phase implies that the
    //                      everything that matches the queries selector fits
    //                      into published set.
    // - _published - Min Heap (also implements IdMap methods)

    var heapOptions = { IdMap: LocalCollection._IdMap };
    self._limit = self._cursorDescription.options.limit;
    self._comparator = comparator;
    self._sorter = sorter;
    self._unpublishedBuffer = new MinMaxHeap(comparator, heapOptions);
    // We need something that can find Max value in addition to IdMap interface
    self._published = new MaxHeap(comparator, heapOptions);
  } else {
    self._limit = 0;
    self._comparator = null;
    self._sorter = null;
    self._unpublishedBuffer = null;
    self._published = new LocalCollection._IdMap;
  }

  // Indicates if it is safe to insert a new document at the end of the buffer
  // for this query. i.e. it is known that there are no documents matching the
  // selector those are not in published or buffer.
  self._safeAppendToBuffer = false;

  self._stopped = false;
  self._stopHandles = [];

  Package['facts-base'] && Package['facts-base'].Facts.incrementServerFact(
    "mongo-livedata", "observe-drivers-oplog", 1);

  self._registerPhaseChange(PHASE.QUERYING);

  self._matcher = options.matcher;
  var projection = self._cursorDescription.options.fields || {};
  self._projectionFn = LocalCollection._compileProjection(projection);
  // Projection function, result of combining important fields for selector and
  // existing fields projection
  self._sharedProjection = self._matcher.combineIntoProjection(projection);
  if (sorter)
    self._sharedProjection = sorter.combineIntoProjection(self._sharedProjection);
  self._sharedProjectionFn = LocalCollection._compileProjection(
    self._sharedProjection);

  self._needToFetch = new LocalCollection._IdMap;
  self._currentlyFetching = null;
  self._fetchGeneration = 0;

  self._requeryWhenDoneThisQuery = false;
  self._writesToCommitWhenWeReachSteady = [];

  // If the oplog handle tells us that it skipped some entries (because it got
  // behind, say), re-poll.
  self._stopHandles.push(self._mongoHandle._oplogHandle.onSkippedEntries(
    finishIfNeedToPollQuery(function () {
      self._needToPollQuery();
    })
  ));

  forEachTrigger(self._cursorDescription, function (trigger) {
    self._stopHandles.push(self._mongoHandle._oplogHandle.onOplogEntry(
      trigger, function (notification) {
        Meteor._noYieldsAllowed(finishIfNeedToPollQuery(function () {
          var op = notification.op;
          if (notification.dropCollection || notification.dropDatabase) {
            // Note: this call is not allowed to block on anything (especially
            // on waiting for oplog entries to catch up) because that will block
            // onOplogEntry!
            self._needToPollQuery();
          } else {
            // All other operators should be handled depending on phase
            if (self._phase === PHASE.QUERYING) {
              self._handleOplogEntryQuerying(op);
            } else {
              self._handleOplogEntrySteadyOrFetching(op);
            }
          }
        }));
      }
    ));
  });

  // XXX ordering w.r.t. everything else?
  self._stopHandles.push(listenAll(
    self._cursorDescription, function (notification) {
      // If we're not in a pre-fire write fence, we don't have to do anything.
      var fence = DDPServer._CurrentWriteFence.get();
      if (!fence || fence.fired)
        return;

      if (fence._oplogObserveDrivers) {
        fence._oplogObserveDrivers[self._id] = self;
        return;
      }

      fence._oplogObserveDrivers = {};
      fence._oplogObserveDrivers[self._id] = self;

      fence.onBeforeFire(function () {
        var drivers = fence._oplogObserveDrivers;
        delete fence._oplogObserveDrivers;

        // This fence cannot fire until we've caught up to "this point" in the
        // oplog, and all observers made it back to the steady state.
        self._mongoHandle._oplogHandle.waitUntilCaughtUp();

        _.each(drivers, function (driver) {
          if (driver._stopped)
            return;

          var write = fence.beginWrite();
          if (driver._phase === PHASE.STEADY) {
            // Make sure that all of the callbacks have made it through the
            // multiplexer and been delivered to ObserveHandles before committing
            // writes.
            driver._multiplexer.onFlush(function () {
              write.committed();
            });
          } else {
            driver._writesToCommitWhenWeReachSteady.push(write);
          }
        });
      });
    }
  ));

  // When Mongo fails over, we need to repoll the query, in case we processed an
  // oplog entry that got rolled back.
  self._stopHandles.push(self._mongoHandle._onFailover(finishIfNeedToPollQuery(
    function () {
      self._needToPollQuery();
    })));

  // Give _observeChanges a chance to add the new ObserveHandle to our
  // multiplexer, so that the added calls get streamed.
  Meteor.defer(finishIfNeedToPollQuery(function () {
    self._runInitialQuery();
  }));
};

_.extend(OplogObserveDriver.prototype, {
  _addPublished: function (id, doc) {
    var self = this;
    Meteor._noYieldsAllowed(function () {
      var fields = _.clone(doc);
      delete fields._id;
      self._published.set(id, self._sharedProjectionFn(doc));
      self._multiplexer.added(id, self._projectionFn(fields));

      // After adding this document, the published set might be overflowed
      // (exceeding capacity specified by limit). If so, push the maximum
      // element to the buffer, we might want to save it in memory to reduce the
      // amount of Mongo lookups in the future.
      if (self._limit && self._published.size() > self._limit) {
        // XXX in theory the size of published is no more than limit+1
        if (self._published.size() !== self._limit + 1) {
          throw new Error("After adding to published, " +
                          (self._published.size() - self._limit) +
                          " documents are overflowing the set");
        }

        var overflowingDocId = self._published.maxElementId();
        var overflowingDoc = self._published.get(overflowingDocId);

        if (EJSON.equals(overflowingDocId, id)) {
          throw new Error("The document just added is overflowing the published set");
        }

        self._published.remove(overflowingDocId);
        self._multiplexer.removed(overflowingDocId);
        self._addBuffered(overflowingDocId, overflowingDoc);
      }
    });
  },
  _removePublished: function (id) {
    var self = this;
    Meteor._noYieldsAllowed(function () {
      self._published.remove(id);
      self._multiplexer.removed(id);
      if (! self._limit || self._published.size() === self._limit)
        return;

      if (self._published.size() > self._limit)
        throw Error("self._published got too big");

      // OK, we are publishing less than the limit. Maybe we should look in the
      // buffer to find the next element past what we were publishing before.

      if (!self._unpublishedBuffer.empty()) {
        // There's something in the buffer; move the first thing in it to
        // _published.
        var newDocId = self._unpublishedBuffer.minElementId();
        var newDoc = self._unpublishedBuffer.get(newDocId);
        self._removeBuffered(newDocId);
        self._addPublished(newDocId, newDoc);
        return;
      }

      // There's nothing in the buffer.  This could mean one of a few things.

      // (a) We could be in the middle of re-running the query (specifically, we
      // could be in _publishNewResults). In that case, _unpublishedBuffer is
      // empty because we clear it at the beginning of _publishNewResults. In
      // this case, our caller already knows the entire answer to the query and
      // we don't need to do anything fancy here.  Just return.
      if (self._phase === PHASE.QUERYING)
        return;

      // (b) We're pretty confident that the union of _published and
      // _unpublishedBuffer contain all documents that match selector. Because
      // _unpublishedBuffer is empty, that means we're confident that _published
      // contains all documents that match selector. So we have nothing to do.
      if (self._safeAppendToBuffer)
        return;

      // (c) Maybe there are other documents out there that should be in our
      // buffer. But in that case, when we emptied _unpublishedBuffer in
      // _removeBuffered, we should have called _needToPollQuery, which will
      // either put something in _unpublishedBuffer or set _safeAppendToBuffer
      // (or both), and it will put us in QUERYING for that whole time. So in
      // fact, we shouldn't be able to get here.

      throw new Error("Buffer inexplicably empty");
    });
  },
  _changePublished: function (id, oldDoc, newDoc) {
    var self = this;
    Meteor._noYieldsAllowed(function () {
      self._published.set(id, self._sharedProjectionFn(newDoc));
      var projectedNew = self._projectionFn(newDoc);
      var projectedOld = self._projectionFn(oldDoc);
      var changed = DiffSequence.makeChangedFields(
        projectedNew, projectedOld);
      if (!_.isEmpty(changed))
        self._multiplexer.changed(id, changed);
    });
  },
  _addBuffered: function (id, doc) {
    var self = this;
    Meteor._noYieldsAllowed(function () {
      self._unpublishedBuffer.set(id, self._sharedProjectionFn(doc));

      // If something is overflowing the buffer, we just remove it from cache
      if (self._unpublishedBuffer.size() > self._limit) {
        var maxBufferedId = self._unpublishedBuffer.maxElementId();

        self._unpublishedBuffer.remove(maxBufferedId);

        // Since something matching is removed from cache (both published set and
        // buffer), set flag to false
        self._safeAppendToBuffer = false;
      }
    });
  },
  // Is called either to remove the doc completely from matching set or to move
  // it to the published set later.
  _removeBuffered: function (id) {
    var self = this;
    Meteor._noYieldsAllowed(function () {
      self._unpublishedBuffer.remove(id);
      // To keep the contract "buffer is never empty in STEADY phase unless the
      // everything matching fits into published" true, we poll everything as
      // soon as we see the buffer becoming empty.
      if (! self._unpublishedBuffer.size() && ! self._safeAppendToBuffer)
        self._needToPollQuery();
    });
  },
  // Called when a document has joined the "Matching" results set.
  // Takes responsibility of keeping _unpublishedBuffer in sync with _published
  // and the effect of limit enforced.
  _addMatching: function (doc) {
    var self = this;
    Meteor._noYieldsAllowed(function () {
      var id = doc._id;
      if (self._published.has(id))
        throw Error("tried to add something already published " + id);
      if (self._limit && self._unpublishedBuffer.has(id))
        throw Error("tried to add something already existed in buffer " + id);

      var limit = self._limit;
      var comparator = self._comparator;
      var maxPublished = (limit && self._published.size() > 0) ?
        self._published.get(self._published.maxElementId()) : null;
      var maxBuffered = (limit && self._unpublishedBuffer.size() > 0)
        ? self._unpublishedBuffer.get(self._unpublishedBuffer.maxElementId())
        : null;
      // The query is unlimited or didn't publish enough documents yet or the
      // new document would fit into published set pushing the maximum element
      // out, then we need to publish the doc.
      var toPublish = ! limit || self._published.size() < limit ||
        comparator(doc, maxPublished) < 0;

      // Otherwise we might need to buffer it (only in case of limited query).
      // Buffering is allowed if the buffer is not filled up yet and all
      // matching docs are either in the published set or in the buffer.
      var canAppendToBuffer = !toPublish && self._safeAppendToBuffer &&
        self._unpublishedBuffer.size() < limit;

      // Or if it is small enough to be safely inserted to the middle or the
      // beginning of the buffer.
      var canInsertIntoBuffer = !toPublish && maxBuffered &&
        comparator(doc, maxBuffered) <= 0;

      var toBuffer = canAppendToBuffer || canInsertIntoBuffer;

      if (toPublish) {
        self._addPublished(id, doc);
      } else if (toBuffer) {
        self._addBuffered(id, doc);
      } else {
        // dropping it and not saving to the cache
        self._safeAppendToBuffer = false;
      }
    });
  },
  // Called when a document leaves the "Matching" results set.
  // Takes responsibility of keeping _unpublishedBuffer in sync with _published
  // and the effect of limit enforced.
  _removeMatching: function (id) {
    var self = this;
    Meteor._noYieldsAllowed(function () {
      if (! self._published.has(id) && ! self._limit)
        throw Error("tried to remove something matching but not cached " + id);

      if (self._published.has(id)) {
        self._removePublished(id);
      } else if (self._unpublishedBuffer.has(id)) {
        self._removeBuffered(id);
      }
    });
  },
  _handleDoc: function (id, newDoc) {
    var self = this;
    Meteor._noYieldsAllowed(function () {
      var matchesNow = newDoc && self._matcher.documentMatches(newDoc).result;

      var publishedBefore = self._published.has(id);
      var bufferedBefore = self._limit && self._unpublishedBuffer.has(id);
      var cachedBefore = publishedBefore || bufferedBefore;

      if (matchesNow && !cachedBefore) {
        self._addMatching(newDoc);
      } else if (cachedBefore && !matchesNow) {
        self._removeMatching(id);
      } else if (cachedBefore && matchesNow) {
        var oldDoc = self._published.get(id);
        var comparator = self._comparator;
        var minBuffered = self._limit && self._unpublishedBuffer.size() &&
          self._unpublishedBuffer.get(self._unpublishedBuffer.minElementId());
        var maxBuffered;

        if (publishedBefore) {
          // Unlimited case where the document stays in published once it
          // matches or the case when we don't have enough matching docs to
          // publish or the changed but matching doc will stay in published
          // anyways.
          //
          // XXX: We rely on the emptiness of buffer. Be sure to maintain the
          // fact that buffer can't be empty if there are matching documents not
          // published. Notably, we don't want to schedule repoll and continue
          // relying on this property.
          var staysInPublished = ! self._limit ||
            self._unpublishedBuffer.size() === 0 ||
            comparator(newDoc, minBuffered) <= 0;

          if (staysInPublished) {
            self._changePublished(id, oldDoc, newDoc);
          } else {
            // after the change doc doesn't stay in the published, remove it
            self._removePublished(id);
            // but it can move into buffered now, check it
            maxBuffered = self._unpublishedBuffer.get(
              self._unpublishedBuffer.maxElementId());

            var toBuffer = self._safeAppendToBuffer ||
                  (maxBuffered && comparator(newDoc, maxBuffered) <= 0);

            if (toBuffer) {
              self._addBuffered(id, newDoc);
            } else {
              // Throw away from both published set and buffer
              self._safeAppendToBuffer = false;
            }
          }
        } else if (bufferedBefore) {
          oldDoc = self._unpublishedBuffer.get(id);
          // remove the old version manually instead of using _removeBuffered so
          // we don't trigger the querying immediately.  if we end this block
          // with the buffer empty, we will need to trigger the query poll
          // manually too.
          self._unpublishedBuffer.remove(id);

          var maxPublished = self._published.get(
            self._published.maxElementId());
          maxBuffered = self._unpublishedBuffer.size() &&
                self._unpublishedBuffer.get(
                  self._unpublishedBuffer.maxElementId());

          // the buffered doc was updated, it could move to published
          var toPublish = comparator(newDoc, maxPublished) < 0;

          // or stays in buffer even after the change
          var staysInBuffer = (! toPublish && self._safeAppendToBuffer) ||
                (!toPublish && maxBuffered &&
                 comparator(newDoc, maxBuffered) <= 0);

          if (toPublish) {
            self._addPublished(id, newDoc);
          } else if (staysInBuffer) {
            // stays in buffer but changes
            self._unpublishedBuffer.set(id, newDoc);
          } else {
            // Throw away from both published set and buffer
            self._safeAppendToBuffer = false;
            // Normally this check would have been done in _removeBuffered but
            // we didn't use it, so we need to do it ourself now.
            if (! self._unpublishedBuffer.size()) {
              self._needToPollQuery();
            }
          }
        } else {
          throw new Error("cachedBefore implies either of publishedBefore or bufferedBefore is true.");
        }
      }
    });
  },
  _fetchModifiedDocuments: function () {
    var self = this;
    Meteor._noYieldsAllowed(function () {
      self._registerPhaseChange(PHASE.FETCHING);
      // Defer, because nothing called from the oplog entry handler may yield,
      // but fetch() yields.
      Meteor.defer(finishIfNeedToPollQuery(function () {
        while (!self._stopped && !self._needToFetch.empty()) {
          if (self._phase === PHASE.QUERYING) {
            // While fetching, we decided to go into QUERYING mode, and then we
            // saw another oplog entry, so _needToFetch is not empty. But we
            // shouldn't fetch these documents until AFTER the query is done.
            break;
          }

          // Being in steady phase here would be surprising.
          if (self._phase !== PHASE.FETCHING)
            throw new Error("phase in fetchModifiedDocuments: " + self._phase);

          self._currentlyFetching = self._needToFetch;
          var thisGeneration = ++self._fetchGeneration;
          self._needToFetch = new LocalCollection._IdMap;
          var waiting = 0;
          var fut = new Future;
          // This loop is safe, because _currentlyFetching will not be updated
          // during this loop (in fact, it is never mutated).
          self._currentlyFetching.forEach(function (cacheKey, id) {
            waiting++;
            self._mongoHandle._docFetcher.fetch(
              self._cursorDescription.collectionName, id, cacheKey,
              finishIfNeedToPollQuery(function (err, doc) {
                try {
                  if (err) {
                    Meteor._debug("Got exception while fetching documents",
                                  err);
                    // If we get an error from the fetcher (eg, trouble
                    // connecting to Mongo), let's just abandon the fetch phase
                    // altogether and fall back to polling. It's not like we're
                    // getting live updates anyway.
                    if (self._phase !== PHASE.QUERYING) {
                      self._needToPollQuery();
                    }
                  } else if (!self._stopped && self._phase === PHASE.FETCHING
                             && self._fetchGeneration === thisGeneration) {
                    // We re-check the generation in case we've had an explicit
                    // _pollQuery call (eg, in another fiber) which should
                    // effectively cancel this round of fetches.  (_pollQuery
                    // increments the generation.)
                    self._handleDoc(id, doc);
                  }
                } finally {
                  waiting--;
                  // Because fetch() never calls its callback synchronously,
                  // this is safe (ie, we won't call fut.return() before the
                  // forEach is done).
                  if (waiting === 0)
                    fut.return();
                }
              }));
          });
          fut.wait();
          // Exit now if we've had a _pollQuery call (here or in another fiber).
          if (self._phase === PHASE.QUERYING)
            return;
          self._currentlyFetching = null;
        }
        // We're done fetching, so we can be steady, unless we've had a
        // _pollQuery call (here or in another fiber).
        if (self._phase !== PHASE.QUERYING)
          self._beSteady();
      }));
    });
  },
  _beSteady: function () {
    var self = this;
    Meteor._noYieldsAllowed(function () {
      self._registerPhaseChange(PHASE.STEADY);
      var writes = self._writesToCommitWhenWeReachSteady;
      self._writesToCommitWhenWeReachSteady = [];
      self._multiplexer.onFlush(function () {
        _.each(writes, function (w) {
          w.committed();
        });
      });
    });
  },
  _handleOplogEntryQuerying: function (op) {
    var self = this;
    Meteor._noYieldsAllowed(function () {
      self._needToFetch.set(idForOp(op), op.ts.toString());
    });
  },
  _handleOplogEntrySteadyOrFetching: function (op) {
    var self = this;
    Meteor._noYieldsAllowed(function () {
      var id = idForOp(op);
      // If we're already fetching this one, or about to, we can't optimize;
      // make sure that we fetch it again if necessary.
      if (self._phase === PHASE.FETCHING &&
          ((self._currentlyFetching && self._currentlyFetching.has(id)) ||
           self._needToFetch.has(id))) {
        self._needToFetch.set(id, op.ts.toString());
        return;
      }

      if (op.op === 'd') {
        if (self._published.has(id) ||
            (self._limit && self._unpublishedBuffer.has(id)))
          self._removeMatching(id);
      } else if (op.op === 'i') {
        if (self._published.has(id))
          throw new Error("insert found for already-existing ID in published");
        if (self._unpublishedBuffer && self._unpublishedBuffer.has(id))
          throw new Error("insert found for already-existing ID in buffer");

        // XXX what if selector yields?  for now it can't but later it could
        // have $where
        if (self._matcher.documentMatches(op.o).result)
          self._addMatching(op.o);
      } else if (op.op === 'u') {
        // Is this a modifier ($set/$unset, which may require us to poll the
        // database to figure out if the whole document matches the selector) or
        // a replacement (in which case we can just directly re-evaluate the
        // selector)?
        var isReplace = !_.has(op.o, '$set') && !_.has(op.o, '$unset');
        // If this modifier modifies something inside an EJSON custom type (ie,
        // anything with EJSON$), then we can't try to use
        // LocalCollection._modify, since that just mutates the EJSON encoding,
        // not the actual object.
        var canDirectlyModifyDoc =
          !isReplace && modifierCanBeDirectlyApplied(op.o);

        var publishedBefore = self._published.has(id);
        var bufferedBefore = self._limit && self._unpublishedBuffer.has(id);

        if (isReplace) {
          self._handleDoc(id, _.extend({_id: id}, op.o));
        } else if ((publishedBefore || bufferedBefore) &&
                   canDirectlyModifyDoc) {
          // Oh great, we actually know what the document is, so we can apply
          // this directly.
          var newDoc = self._published.has(id)
            ? self._published.get(id) : self._unpublishedBuffer.get(id);
          newDoc = EJSON.clone(newDoc);

          newDoc._id = id;
          try {
            LocalCollection._modify(newDoc, op.o);
          } catch (e) {
            if (e.name !== "MinimongoError")
              throw e;
            // We didn't understand the modifier.  Re-fetch.
            self._needToFetch.set(id, op.ts.toString());
            if (self._phase === PHASE.STEADY) {
              self._fetchModifiedDocuments();
            }
            return;
          }
          self._handleDoc(id, self._sharedProjectionFn(newDoc));
        } else if (!canDirectlyModifyDoc ||
                   self._matcher.canBecomeTrueByModifier(op.o) ||
                   (self._sorter && self._sorter.affectedByModifier(op.o))) {
          self._needToFetch.set(id, op.ts.toString());
          if (self._phase === PHASE.STEADY)
            self._fetchModifiedDocuments();
        }
      } else {
        throw Error("XXX SURPRISING OPERATION: " + op);
      }
    });
  },
  // Yields!
  _runInitialQuery: function () {
    var self = this;
    if (self._stopped)
      throw new Error("oplog stopped surprisingly early");

    self._runQuery({initial: true});  // yields

    if (self._stopped)
      return;  // can happen on queryError

    // Allow observeChanges calls to return. (After this, it's possible for
    // stop() to be called.)
    self._multiplexer.ready();

    self._doneQuerying();  // yields
  },

  // In various circumstances, we may just want to stop processing the oplog and
  // re-run the initial query, just as if we were a PollingObserveDriver.
  //
  // This function may not block, because it is called from an oplog entry
  // handler.
  //
  // XXX We should call this when we detect that we've been in FETCHING for "too
  // long".
  //
  // XXX We should call this when we detect Mongo failover (since that might
  // mean that some of the oplog entries we have processed have been rolled
  // back). The Node Mongo driver is in the middle of a bunch of huge
  // refactorings, including the way that it notifies you when primary
  // changes. Will put off implementing this until driver 1.4 is out.
  _pollQuery: function () {
    var self = this;
    Meteor._noYieldsAllowed(function () {
      if (self._stopped)
        return;

      // Yay, we get to forget about all the things we thought we had to fetch.
      self._needToFetch = new LocalCollection._IdMap;
      self._currentlyFetching = null;
      ++self._fetchGeneration;  // ignore any in-flight fetches
      self._registerPhaseChange(PHASE.QUERYING);

      // Defer so that we don't yield.  We don't need finishIfNeedToPollQuery
      // here because SwitchedToQuery is not thrown in QUERYING mode.
      Meteor.defer(function () {
        self._runQuery();
        self._doneQuerying();
      });
    });
  },

  // Yields!
  _runQuery: function (options) {
    var self = this;
    options = options || {};
    var newResults, newBuffer;

    // This while loop is just to retry failures.
    while (true) {
      // If we've been stopped, we don't have to run anything any more.
      if (self._stopped)
        return;

      newResults = new LocalCollection._IdMap;
      newBuffer = new LocalCollection._IdMap;

      // Query 2x documents as the half excluded from the original query will go
      // into unpublished buffer to reduce additional Mongo lookups in cases
      // when documents are removed from the published set and need a
      // replacement.
      // XXX needs more thought on non-zero skip
      // XXX 2 is a "magic number" meaning there is an extra chunk of docs for
      // buffer if such is needed.
      var cursor = self._cursorForQuery({ limit: self._limit * 2 });
      try {
        cursor.forEach(function (doc, i) {  // yields
          if (!self._limit || i < self._limit) {
            newResults.set(doc._id, doc);
          } else {
            newBuffer.set(doc._id, doc);
          }
        });
        break;
      } catch (e) {
        if (options.initial && typeof(e.code) === 'number') {
          // This is an error document sent to us by mongod, not a connection
          // error generated by the client. And we've never seen this query work
          // successfully. Probably it's a bad selector or something, so we
          // should NOT retry. Instead, we should halt the observe (which ends
          // up calling `stop` on us).
          self._multiplexer.queryError(e);
          return;
        }

        // During failover (eg) if we get an exception we should log and retry
        // instead of crashing.
        Meteor._debug("Got exception while polling query", e);
        Meteor._sleepForMs(100);
      }
    }

    if (self._stopped)
      return;

    self._publishNewResults(newResults, newBuffer);
  },

  // Transitions to QUERYING and runs another query, or (if already in QUERYING)
  // ensures that we will query again later.
  //
  // This function may not block, because it is called from an oplog entry
  // handler. However, if we were not already in the QUERYING phase, it throws
  // an exception that is caught by the closest surrounding
  // finishIfNeedToPollQuery call; this ensures that we don't continue running
  // close that was designed for another phase inside PHASE.QUERYING.
  //
  // (It's also necessary whenever logic in this file yields to check that other
  // phases haven't put us into QUERYING mode, though; eg,
  // _fetchModifiedDocuments does this.)
  _needToPollQuery: function () {
    var self = this;
    Meteor._noYieldsAllowed(function () {
      if (self._stopped)
        return;

      // If we're not already in the middle of a query, we can query now
      // (possibly pausing FETCHING).
      if (self._phase !== PHASE.QUERYING) {
        self._pollQuery();
        throw new SwitchedToQuery;
      }

      // We're currently in QUERYING. Set a flag to ensure that we run another
      // query when we're done.
      self._requeryWhenDoneThisQuery = true;
    });
  },

  // Yields!
  _doneQuerying: function () {
    var self = this;

    if (self._stopped)
      return;
    self._mongoHandle._oplogHandle.waitUntilCaughtUp();  // yields
    if (self._stopped)
      return;
    if (self._phase !== PHASE.QUERYING)
      throw Error("Phase unexpectedly " + self._phase);

    Meteor._noYieldsAllowed(function () {
      if (self._requeryWhenDoneThisQuery) {
        self._requeryWhenDoneThisQuery = false;
        self._pollQuery();
      } else if (self._needToFetch.empty()) {
        self._beSteady();
      } else {
        self._fetchModifiedDocuments();
      }
    });
  },

  _cursorForQuery: function (optionsOverwrite) {
    var self = this;
    return Meteor._noYieldsAllowed(function () {
      // The query we run is almost the same as the cursor we are observing,
      // with a few changes. We need to read all the fields that are relevant to
      // the selector, not just the fields we are going to publish (that's the
      // "shared" projection). And we don't want to apply any transform in the
      // cursor, because observeChanges shouldn't use the transform.
      var options = _.clone(self._cursorDescription.options);

      // Allow the caller to modify the options. Useful to specify different
      // skip and limit values.
      _.extend(options, optionsOverwrite);

      options.fields = self._sharedProjection;
      delete options.transform;
      // We are NOT deep cloning fields or selector here, which should be OK.
      var description = new CursorDescription(
        self._cursorDescription.collectionName,
        self._cursorDescription.selector,
        options);
      return new Cursor(self._mongoHandle, description);
    });
  },

  // Replace self._published with newResults (both are IdMaps), invoking observe
  // callbacks on the multiplexer.
  // Replace self._unpublishedBuffer with newBuffer.
  //
  // XXX This is very similar to LocalCollection._diffQueryUnorderedChanges. We
  // should really: (a) Unify IdMap and OrderedDict into Unordered/OrderedDict
  // (b) Rewrite diff.js to use these classes instead of arrays and objects.
  _publishNewResults: function (newResults, newBuffer) {
    var self = this;
    Meteor._noYieldsAllowed(function () {

      // If the query is limited and there is a buffer, shut down so it doesn't
      // stay in a way.
      if (self._limit) {
        self._unpublishedBuffer.clear();
      }

      // First remove anything that's gone. Be careful not to modify
      // self._published while iterating over it.
      var idsToRemove = [];
      self._published.forEach(function (doc, id) {
        if (!newResults.has(id))
          idsToRemove.push(id);
      });
      _.each(idsToRemove, function (id) {
        self._removePublished(id);
      });

      // Now do adds and changes.
      // If self has a buffer and limit, the new fetched result will be
      // limited correctly as the query has sort specifier.
      newResults.forEach(function (doc, id) {
        self._handleDoc(id, doc);
      });

      // Sanity-check that everything we tried to put into _published ended up
      // there.
      // XXX if this is slow, remove it later
      if (self._published.size() !== newResults.size()) {
        console.error('The Mongo server and the Meteor query disagree on how ' +
          'many documents match your query. Cursor description: ',
          self._cursorDescription);
        throw Error(
          "The Mongo server and the Meteor query disagree on how " +
            "many documents match your query. Maybe it is hitting a Mongo " +
            "edge case? The query is: " +
            EJSON.stringify(self._cursorDescription.selector));
      }
      self._published.forEach(function (doc, id) {
        if (!newResults.has(id))
          throw Error("_published has a doc that newResults doesn't; " + id);
      });

      // Finally, replace the buffer
      newBuffer.forEach(function (doc, id) {
        self._addBuffered(id, doc);
      });

      self._safeAppendToBuffer = newBuffer.size() < self._limit;
    });
  },

  // This stop function is invoked from the onStop of the ObserveMultiplexer, so
  // it shouldn't actually be possible to call it until the multiplexer is
  // ready.
  //
  // It's important to check self._stopped after every call in this file that
  // can yield!
  stop: function () {
    var self = this;
    if (self._stopped)
      return;
    self._stopped = true;
    _.each(self._stopHandles, function (handle) {
      handle.stop();
    });

    // Note: we *don't* use multiplexer.onFlush here because this stop
    // callback is actually invoked by the multiplexer itself when it has
    // determined that there are no handles left. So nothing is actually going
    // to get flushed (and it's probably not valid to call methods on the
    // dying multiplexer).
    _.each(self._writesToCommitWhenWeReachSteady, function (w) {
      w.committed();  // maybe yields?
    });
    self._writesToCommitWhenWeReachSteady = null;

    // Proactively drop references to potentially big things.
    self._published = null;
    self._unpublishedBuffer = null;
    self._needToFetch = null;
    self._currentlyFetching = null;
    self._oplogEntryHandle = null;
    self._listenersHandle = null;

    Package['facts-base'] && Package['facts-base'].Facts.incrementServerFact(
      "mongo-livedata", "observe-drivers-oplog", -1);
  },

  _registerPhaseChange: function (phase) {
    var self = this;
    Meteor._noYieldsAllowed(function () {
      var now = new Date;

      if (self._phase) {
        var timeDiff = now - self._phaseStartTime;
        Package['facts-base'] && Package['facts-base'].Facts.incrementServerFact(
          "mongo-livedata", "time-spent-in-" + self._phase + "-phase", timeDiff);
      }

      self._phase = phase;
      self._phaseStartTime = now;
    });
  }
});

// Does our oplog tailing code support this cursor? For now, we are being very
// conservative and allowing only simple queries with simple options.
// (This is a "static method".)
OplogObserveDriver.cursorSupported = function (cursorDescription, matcher) {
  // First, check the options.
  var options = cursorDescription.options;

  // Did the user say no explicitly?
  // underscored version of the option is COMPAT with 1.2
  if (options.disableOplog || options._disableOplog)
    return false;

  // skip is not supported: to support it we would need to keep track of all
  // "skipped" documents or at least their ids.
  // limit w/o a sort specifier is not supported: current implementation needs a
  // deterministic way to order documents.
  if (options.skip || (options.limit && !options.sort)) return false;

  // If a fields projection option is given check if it is supported by
  // minimongo (some operators are not supported).
  if (options.fields) {
    try {
      LocalCollection._checkSupportedProjection(options.fields);
    } catch (e) {
      if (e.name === "MinimongoError") {
        return false;
      } else {
        throw e;
      }
    }
  }

  // We don't allow the following selectors:
  //   - $where (not confident that we provide the same JS environment
  //             as Mongo, and can yield!)
  //   - $near (has "interesting" properties in MongoDB, like the possibility
  //            of returning an ID multiple times, though even polling maybe
  //            have a bug there)
  //           XXX: once we support it, we would need to think more on how we
  //           initialize the comparators when we create the driver.
  return !matcher.hasWhere() && !matcher.hasGeoQuery();
};

var modifierCanBeDirectlyApplied = function (modifier) {
  return _.all(modifier, function (fields, operation) {
    return _.all(fields, function (value, field) {
      return !/EJSON\$/.test(field);
    });
  });
};

MongoInternals.OplogObserveDriver = OplogObserveDriver;


const [ startKey, endKey ] = this.renderRange

const startIndex = startKey ? blocks.findIndex(block => block.key === startKey) : 0

const endIndex = endKey ? blocks.findIndex(block => block.key === endKey) + 1 : blocks.length

const needRenderBlocks = blocks.slice(startIndex, endIndex)

import virtualize from 'snabbdom-virtualize/strings'
import { CLASS_OR_ID, IMAGE_EXT_REG } from '../config'
import { conflict, isLengthEven, union, isEven, getUniqueId, loadImage, getImageSrc } from '../utils'
import { insertAfter, operateClassName } from '../utils/domManipulate'
import { tokenizer } from './parse'
import { validEmoji } from '../emojis'

const snabbdom = require('snabbdom')
const patch = snabbdom.init([ // Init patch function with chosen modules
  require('snabbdom/modules/class').default, // makes it easy to toggle classes
  require('snabbdom/modules/attributes').default,
  require('snabbdom/modules/props').default, // for setting properties on DOM elements
  require('snabbdom/modules/dataset').default
])
const h = require('snabbdom/h').default // helper function for creating vnodes
const toHTML = require('snabbdom-to-html')
const toVNode = require('snabbdom/tovnode').default

const PRE_BLOCK_HASH = {
  'code': `.${CLASS_OR_ID['AG_CODE_BLOCK']}`,
  'html': `.${CLASS_OR_ID['AG_HTML_BLOCK']}`,
  'frontmatter': `.${CLASS_OR_ID['AG_FRONT_MATTER']}`
}

class StateRender {
  constructor (eventCenter) {
    this.eventCenter = eventCenter
    this.loadImageMap = new Map()
    this.container = null
  }

  setContainer (container) {
    this.container = container
  }

  checkConflicted (block, token, cursor) {
    const { start, end } = cursor
    const key = block.key
    const { start: tokenStart, end: tokenEnd } = token.range

    if (key !== start.key && key !== end.key) {
      return false
    } else if (key === start.key && key !== end.key) {
      return conflict([tokenStart, tokenEnd], [start.offset, start.offset])
    } else if (key !== start.key && key === end.key) {
      return conflict([tokenStart, tokenEnd], [end.offset, end.offset])
    } else {
      return conflict([tokenStart, tokenEnd], [start.offset, start.offset]) ||
        conflict([tokenStart, tokenEnd], [end.offset, end.offset])
    }
  }

  getClassName (outerClass, block, token, cursor) {
    return outerClass || (this.checkConflicted(block, token, cursor) ? CLASS_OR_ID['AG_GRAY'] : CLASS_OR_ID['AG_HIDE'])
  }

  getHighlightClassName (active) {
    return active ? CLASS_OR_ID['AG_HIGHLIGHT'] : CLASS_OR_ID['AG_SELECTION']
  }

  getSelector (block, cursor, activeBlocks) {
    const type = block.type === 'hr' ? 'p' : block.type
    const isActive = activeBlocks.some(b => b.key === block.key) || block.key === cursor.start.key

    let selector = `${type}#${block.key}.${CLASS_OR_ID['AG_PARAGRAPH']}`
    if (isActive) {
      selector += `.${CLASS_OR_ID['AG_ACTIVE']}`
    }
    if (type === 'span') {
      selector += `.${CLASS_OR_ID['AG_LINE']}`
    }
    if (block.temp) {
      selector += `.${CLASS_OR_ID['AG_TEMP']}`
    }
    return selector
  }

  renderLeafBlock (block, cursor, activeBlocks, matches) {
    let selector = this.getSelector(block, cursor, activeBlocks)
    // highlight search key in block
    const highlights = matches.filter(m => m.key === block.key)
    const { text, type, align, htmlContent, icon, checked, key, lang, functionType, codeBlockStyle } = block
    const data = {
      attrs: {},
      dataset: {}
    }
    let children = ''

    if (text) {
      children = tokenizer(text, highlights).reduce((acc, token) => [...acc, ...this[token.type](h, cursor, block, token)], [])
    }

    if (/th|td/.test(type) && align) {
      Object.assign(data.attrs, {
        style: `text-align:${align}`
      })
    } else if (type === 'div' && htmlContent !== undefined) {
      selector += `.${CLASS_OR_ID['AG_HTML_PREVIEW']}`
      Object.assign(data.attrs, {
        contenteditable: 'false'
      })
      children = virtualize(htmlContent)
    } else if (type === 'svg' && icon) {
      selector += '.icon'
      Object.assign(data.attrs, {
        'aria-hidden': 'true'
      })
      children = [
        h('use', {
          attrs: {
            'xlink:href': `#${icon}`
          }
        })
      ]
    } else if (/^h/.test(type)) {
      if (/^h\d$/.test(type)) {
        Object.assign(data.dataset, {
          head: type
        })
      }
      Object.assign(data.dataset, {
        role: type
      })
    } else if (type === 'input') {
      Object.assign(data.attrs, {
        type: 'checkbox'
      })
      selector = `${type}#${key}.${CLASS_OR_ID['AG_TASK_LIST_ITEM_CHECKBOX']}`
      if (checked) {
        Object.assign(data.attrs, {
          checked: true
        })
        selector += `.${CLASS_OR_ID['AG_CHECKBOX_CHECKED']}`
      }
      children = ''
    } else if (type === 'pre') {
      if (lang) {
        Object.assign(data.dataset, {
          lang
        })
      }
      if (codeBlockStyle) {
        Object.assign(data.dataset, {
          codeBlockStyle
        })
      }
      selector += `.${CLASS_OR_ID['AG_CODEMIRROR_BLOCK']}`
      selector += PRE_BLOCK_HASH[functionType]
      if (functionType !== 'frontmatter') {
        children = ''
      }
    } else if (type === 'span' && functionType === 'frontmatter') {
      selector += `.${CLASS_OR_ID['AG_FRONT_MATTER_LINE']}`
      children = text
    }

    return h(selector, data, children)
  }

  renderContainerBlock (block, cursor, activeBlocks, matches) {
    let selector = this.getSelector(block, cursor, activeBlocks)
    const data = {
      attrs: {},
      dataset: {}
    }
    // handle `div` block
    if (/div/.test(block.type)) {
      if (block.toolBarType) {
        selector += `.${'ag-tool-' + block.toolBarType}.${CLASS_OR_ID['AG_TOOL_BAR']}`
      }
      if (block.functionType) {
        selector += `.${'ag-function-' + block.functionType}`
      }
      if (block.editable !== undefined && !block.editable) {
        Object.assign(data.attrs, { contenteditable: 'false' })
      }
    }
    // handle `figure` block
    if (block.type === 'figure') {
      if (block.functionType === 'html') { // HTML Block
        Object.assign(data.dataset, { role: block.functionType.toUpperCase() })
      }
    }
    // hanle list block
    if (/ul|ol/.test(block.type) && block.listType) {
      switch (block.listType) {
        case 'order':
          selector += `.${CLASS_OR_ID['AG_ORDER_LIST']}`
          break
        case 'bullet':
          selector += `.${CLASS_OR_ID['AG_BULLET_LIST']}`
          break
        case 'task':
          selector += `.${CLASS_OR_ID['AG_TASK_LIST']}`
          break
        default:
          break
      }
    }
    if (block.type === 'li' && block.label) {
      const { label } = block
      const { align } = activeBlocks[0]

      if (align && block.label === align) {
        selector += '.active'
      }
      Object.assign(data.dataset, { label })
    }
    if (block.type === 'li' && block.listItemType) {
      switch (block.listItemType) {
        case 'order':
          selector += `.${CLASS_OR_ID['AG_ORDER_LIST_ITEM']}`
          break
        case 'bullet':
          selector += `.${CLASS_OR_ID['AG_BULLET_LIST_ITEM']}`
          break
        case 'task':
          selector += `.${CLASS_OR_ID['AG_TASK_LIST_ITEM']}`
          break
        default:
          break
      }
      selector += block.isLooseListItem ? `.${CLASS_OR_ID['AG_LOOSE_LIST_ITEM']}` : `.${CLASS_OR_ID['AG_TIGHT_LIST_ITEM']}`
    }
    if (block.type === 'ol') {
      Object.assign(data.attrs, { start: block.start })
    }
    if (block.type === 'pre' && block.functionType === 'frontmatter') {
      Object.assign(data.dataset, { role: 'YAML' })
      selector += `.${CLASS_OR_ID['AG_FRONT_MATTER']}`
    }

    return h(selector, data, block.children.map(child => this.renderBlock(child, cursor, activeBlocks, matches)))
  }

  /**
   * [renderBlock render one block, no matter it is a container block or text block]
   */
  renderBlock (block, cursor, activeBlocks, matches) {
    const method = block.children.length > 0 ? 'renderContainerBlock' : 'renderLeafBlock'

    return this[method](block, cursor, activeBlocks, matches)
  }

  render (blocks, cursor, activeBlocks, matches) {
    const selector = `div#${CLASS_OR_ID['AG_EDITOR_ID']}`

    const children = blocks.map(block => {
      return this.renderBlock(block, cursor, activeBlocks, matches)
    })

    const newVdom = h(selector, children)
    const rootDom = document.querySelector(selector) || this.container
    const oldVdom = toVNode(rootDom)

    patch(oldVdom, newVdom)
  }

  partialRender (blocks, cursor, activeBlocks, matches, startKey, endKey) {
    const cursorOutMostBlock = activeBlocks[activeBlocks.length - 1]
    // If cursor is not in render blocks, need to render cursor block independently
    const needRenderCursorBlock = blocks.indexOf(cursorOutMostBlock) === -1
    const newVnode = h('section', blocks.map(block => this.renderBlock(block, cursor, activeBlocks, matches)))
    const html = toHTML(newVnode).replace(/^<section>([\s\S]+?)<\/section>$/, '$1')

    const needToRemoved = []
    const firstOldDom = startKey
      ? document.querySelector(`#${startKey}`)
      : document.querySelector(`div#${CLASS_OR_ID['AG_EDITOR_ID']}`).firstElementChild
    needToRemoved.push(firstOldDom)
    let nextSibling = firstOldDom.nextElementSibling
    while (nextSibling && nextSibling.id !== endKey) {
      needToRemoved.push(nextSibling)
      nextSibling = nextSibling.nextElementSibling
    }
    nextSibling && needToRemoved.push(nextSibling)

    firstOldDom.insertAdjacentHTML('beforebegin', html)

    Array.from(needToRemoved).forEach(dom => dom.remove())

    // Render cursor block independently
    if (needRenderCursorBlock) {
      const { key } = cursorOutMostBlock
      const cursorDom = document.querySelector(`#${key}`)
      if (cursorDom) {
        const oldCursorVnode = toVNode(cursorDom)
        const newCursorVnode = this.renderBlock(cursorOutMostBlock, cursor, activeBlocks, matches)
        patch(oldCursorVnode, newCursorVnode)
      }
    }
  }

  hr (h, cursor, block, token, outerClass) {
    const { start, end } = token.range
    const content = this.highlight(h, block, start, end, token)
    return [
      h(`span.${CLASS_OR_ID['AG_GRAY']}.${CLASS_OR_ID['AG_REMOVE']}`, content)
    ]
  }

  header (h, cursor, block, token, outerClass) {
    const className = this.getClassName(outerClass, block, token, cursor)
    const { start, end } = token.range
    const content = this.highlight(h, block, start, end, token)
    return [
      h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, content)
    ]
  }

  ['tail_header'] (h, cursor, block, token, outerClass) {
    const className = this.getClassName(outerClass, block, token, cursor)
    const { start, end } = token.range
    const content = this.highlight(h, block, start, end, token)
    if (/^h\d$/.test(block.type)) {
      return [
        h(`span.${className}`, content)
      ]
    } else {
      return content
    }
  }

  ['hard_line_break'] (h, cursor, block, token, outerClass) {
    const className = CLASS_OR_ID['AG_HARD_LINE_BREAK']
    const content = [ token.spaces ]
    if (block.type === 'span' && block.nextSibling) {
      return [
        h(`span.${className}`, content)
      ]
    } else {
      return content
    }
  }

  ['code_fense'] (h, cursor, block, token, outerClass) {
    const { start, end } = token.range
    const { marker } = token

    const markerContent = this.highlight(h, block, start, start + marker.length, token)
    const content = this.highlight(h, block, start + marker.length, end, token)

    return [
      h(`span.${CLASS_OR_ID['AG_GRAY']}`, markerContent),
      h(`span.${CLASS_OR_ID['AG_LANGUAGE']}`, content)
    ]
  }

  backlash (h, cursor, block, token, outerClass) {
    const className = this.getClassName(outerClass, block, token, cursor)
    const { start, end } = token.range
    const content = this.highlight(h, block, start, end, token)

    return [
      h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, content)
    ]
  }

  ['display_math'] (h, cursor, block, token, outerClass) {
    const className = this.getClassName(outerClass, block, token, cursor)
    const { start, end } = token.range
    const { marker } = token

    const startMarker = this.highlight(h, block, start, start + marker.length, token)
    const endMarker = this.highlight(h, block, end - marker.length, end, token)
    const content = this.highlight(h, block, start + marker.length, end - marker.length, token)

    const { content: math, type } = token

    return [
      h(`span.${className}.${CLASS_OR_ID['AG_MATH_MARKER']}`, startMarker),
      h(`span.${className}.${CLASS_OR_ID['AG_MATH']}`, [
        h(`span.${CLASS_OR_ID['AG_MATH_TEXT']}`, content),
        h(`span.${CLASS_OR_ID['AG_MATH_RENDER']}`, {
          dataset: { math, type },
          attrs: { contenteditable: 'false' }
        }, 'Loading')
      ]),
      h(`span.${className}.${CLASS_OR_ID['AG_MATH_MARKER']}`, endMarker)
    ]
  }

  ['inline_math'] (h, cursor, block, token, outerClass) {
    return this['display_math'](h, cursor, block, token, outerClass)
  }

  ['inline_code'] (h, cursor, block, token, outerClass) {
    const className = this.getClassName(outerClass, block, token, cursor)
    const { marker } = token
    const { start, end } = token.range

    const startMarker = this.highlight(h, block, start, start + marker.length, token)
    const endMarker = this.highlight(h, block, end - marker.length, end, token)
    const content = this.highlight(h, block, start + marker.length, end - marker.length, token)

    return [
      h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, startMarker),
      h('code', content),
      h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, endMarker)
    ]
  }

  // change text to highlight vdom
  highlight (h, block, rStart, rEnd, token) {
    const { text } = block
    const { highlights } = token
    let result = []
    let unions = []
    let pos = rStart

    if (highlights) {
      for (const light of highlights) {
        const un = union({ start: rStart, end: rEnd }, light)
        if (un) unions.push(un)
      }
    }

    if (unions.length) {
      for (const u of unions) {
        const { start, end, active } = u
        const className = this.getHighlightClassName(active)

        if (pos < start) {
          result.push(text.substring(pos, start))
        }

        result.push(h(`span.${className}`, text.substring(start, end)))
        pos = end
      }
      if (pos < rEnd) {
        result.push(block.text.substring(pos, rEnd))
      }
    } else {
      result = [ text.substring(rStart, rEnd) ]
    }

    return result
  }
  // render token of text type to vdom.
  text (h, cursor, block, token) {
    const { start, end } = token.range
    return this.highlight(h, block, start, end, token)
  }
  // render token of emoji to vdom
  emoji (h, cursor, block, token, outerClass) {
    const { start: rStart, end: rEnd } = token.range
    const className = this.getClassName(outerClass, block, token, cursor)
    const validation = validEmoji(token.content)
    const finalClass = validation ? className : CLASS_OR_ID['AG_WARN']
    const CONTENT_CLASSNAME = `span.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKED_TEXT']}`
    let startMarkerCN = `span.${finalClass}.${CLASS_OR_ID['AG_EMOJI_MARKER']}`
    let endMarkerCN = startMarkerCN
    let content = token.content
    let pos = rStart + token.marker.length

    if (token.highlights && token.highlights.length) {
      content = []
      for (const light of token.highlights) {
        let { start, end, active } = light
        const HIGHLIGHT_CLASSNAME = this.getHighlightClassName(active)
        if (start === rStart) {
          startMarkerCN += `.${HIGHLIGHT_CLASSNAME}`
          start++
        }
        if (end === rEnd) {
          endMarkerCN += `.${HIGHLIGHT_CLASSNAME}`
          end--
        }
        if (pos < start) {
          content.push(block.text.substring(pos, start))
        }
        if (start < end) {
          content.push(h(`span.${HIGHLIGHT_CLASSNAME}`, block.text.substring(start, end)))
        }
        pos = end
      }
      if (pos < rEnd - token.marker.length) {
        content.push(block.text.substring(pos, rEnd - 1))
      }
    }

    const emojiVdom = validation
      ? h(CONTENT_CLASSNAME, {
        dataset: {
          emoji: validation.emoji
        }
      }, content)
      : h(CONTENT_CLASSNAME, content)

    return [
      h(startMarkerCN, token.marker),
      emojiVdom,
      h(endMarkerCN, token.marker)
    ]
  }

  // render factory of `del`,`em`,`strong`
  delEmStrongFac (type, h, cursor, block, token, outerClass) {
    const className = this.getClassName(outerClass, block, token, cursor)
    const COMMON_MARKER = `span.${className}.${CLASS_OR_ID['AG_REMOVE']}`
    const { marker } = token
    const { start, end } = token.range
    const backlashStart = end - marker.length - token.backlash.length
    const content = [
      ...token.children.reduce((acc, to) => {
        const chunk = this[to.type](h, cursor, block, to, className)
        return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
      }, []),
      ...this.backlashInToken(token.backlash, className, backlashStart, token)
    ]
    const startMarker = this.highlight(h, block, start, start + marker.length, token)
    const endMarker = this.highlight(h, block, end - marker.length, end, token)

    if (isLengthEven(token.backlash)) {
      return [
        h(COMMON_MARKER, startMarker),
        h(type, content),
        h(COMMON_MARKER, endMarker)
      ]
    } else {
      return [
        ...startMarker,
        ...content,
        ...endMarker
      ]
    }
  }
  // TODO HIGHLIGHT
  backlashInToken (backlashes, outerClass, start, token) {
    const { highlights = [] } = token
    const chunks = backlashes.split('')
    const len = chunks.length
    const result = []
    let i

    for (i = 0; i < len; i++) {
      const chunk = chunks[i]
      const light = highlights.filter(light => union({ start: start + i, end: start + i + 1 }, light))
      let selector = 'span'
      if (light.length) {
        const className = this.getHighlightClassName(light[0].active)
        selector += `.${className}`
      }
      if (isEven(i)) {
        result.push(
          h(`${selector}.${outerClass}`, chunk)
        )
      } else {
        result.push(
          h(`${selector}.${CLASS_OR_ID['AG_BACKLASH']}`, chunk)
        )
      }
    }

    return result
  }

  loadImageAsync (src, alt, className, imageClass) {
    let id
    let isSuccess

    if (!this.loadImageMap.has(src)) {
      id = getUniqueId()
      loadImage(src)
        .then(url => {
          const imageWrapper = document.querySelector(`#${id}`)
          const img = document.createElement('img')
          img.src = url
          if (alt) img.alt = alt
          if (imageClass) {
            img.classList.add(imageClass)
          }
          if (imageWrapper) {
            insertAfter(img, imageWrapper)
            operateClassName(imageWrapper, 'add', className)
          }
          this.loadImageMap.set(src, {
            id,
            isSuccess: true
          })
        })
        .catch(() => {
          const imageWrapper = document.querySelector(`#${id}`)
          if (imageWrapper) {
            operateClassName(imageWrapper, 'add', CLASS_OR_ID['AG_IMAGE_FAIL'])
          }
          this.loadImageMap.set(src, {
            id,
            isSuccess: false
          })
        })
    } else {
      const imageInfo = this.loadImageMap.get(src)
      id = imageInfo.id
      isSuccess = imageInfo.isSuccess
    }

    return { id, isSuccess }
  }

  // I dont want operate dom directly, is there any better method? need help!
  image (h, cursor, block, token, outerClass) {
    const { eventCenter } = this
    const { start: cursorStart, end: cursorEnd } = cursor
    const { start, end } = token.range
    if (
      cursorStart.key === cursorEnd.key &&
      cursorStart.offset === cursorEnd.offset &&
      cursorStart.offset === end - 1 &&
      !IMAGE_EXT_REG.test(token.src)
    ) {
      eventCenter.dispatch('image-path', token.src)
    }

    const className = this.getClassName(outerClass, block, token, cursor)
    const imageClass = CLASS_OR_ID['AG_IMAGE_MARKED_TEXT']

    const titleContent = this.highlight(h, block, start, start + 2 + token.title.length, token)

    const srcContent = this.highlight(
      h, block,
      start + 2 + token.title.length + token.backlash.first.length + 2,
      start + 2 + token.title.length + token.backlash.first.length + 2 + token.src.length,
      token
    )

    const firstBracketContent = this.highlight(h, block, start, start + 2, token)

    const secondBracketContent = this.highlight(
      h, block,
      start + 2 + token.title.length + token.backlash.first.length,
      start + 2 + token.title.length + token.backlash.first.length + 2,
      token
    )

    const lastBracketContent = this.highlight(h, block, end - 1, end, token)

    const firstBacklashStart = start + 2 + token.title.length

    const secondBacklashStart = end - 1 - token.backlash.second.length

    if (isLengthEven(token.backlash.first) && isLengthEven(token.backlash.second)) {
      let id
      let isSuccess
      let selector
      const src = getImageSrc(token.src + encodeURI(token.backlash.second))
      const alt = token.title + encodeURI(token.backlash.first)

      if (src) {
        ({ id, isSuccess } = this.loadImageAsync(src, alt, className))
      }

      selector = id ? `span#${id}.${imageClass}.${CLASS_OR_ID['AG_REMOVE']}` : `span.${imageClass}.${CLASS_OR_ID['AG_REMOVE']}`

      if (isSuccess) {
        selector += `.${className}`
      } else {
        selector += `.${CLASS_OR_ID['AG_IMAGE_FAIL']}`
      }
      const children = [
        ...titleContent,
        ...this.backlashInToken(token.backlash.first, className, firstBacklashStart, token),
        ...secondBracketContent,
        h(`span.${CLASS_OR_ID['AG_IMAGE_SRC']}`, srcContent),
        ...this.backlashInToken(token.backlash.second, className, secondBacklashStart, token),
        ...lastBracketContent
      ]

      return isSuccess
        ? [
          h(selector, children),
          h('img', { props: { alt, src } })
        ]
        : [h(selector, children)]
    } else {
      return [
        ...firstBracketContent,
        ...token.children.reduce((acc, to) => {
          const chunk = this[to.type](h, cursor, block, to, className)
          return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
        }, []),
        ...this.backlashInToken(token.backlash.first, className, firstBacklashStart, token),
        ...secondBracketContent,
        ...srcContent,
        ...this.backlashInToken(token.backlash.second, className, secondBacklashStart, token),
        ...lastBracketContent
      ]
    }
  }

  // html_image
  ['html_image'] (h, cursor, block, token, outerClass) {
    const className = this.getClassName(outerClass, block, token, cursor)
    const imageClass = CLASS_OR_ID['AG_IMAGE_MARKED_TEXT']
    const { start, end } = token.range
    const tag = this.highlight(h, block, start, end, token)
    const { src: rawSrc, alt } = token
    const src = getImageSrc(rawSrc)
    let id
    let isSuccess
    let selector
    if (src) {
      ({ id, isSuccess } = this.loadImageAsync(src, alt, className, CLASS_OR_ID['AG_COPY_REMOVE']))
    }
    selector = id ? `span#${id}.${imageClass}.${CLASS_OR_ID['AG_HTML_TAG']}` : `span.${imageClass}.${CLASS_OR_ID['AG_HTML_TAG']}`
    selector += `.${CLASS_OR_ID['AG_OUTPUT_REMOVE']}`
    if (isSuccess) {
      selector += `.${className}`
    } else {
      selector += `.${CLASS_OR_ID['AG_IMAGE_FAIL']}`
    }

    return isSuccess
      ? [
        h(selector, tag),
        h(`img.${CLASS_OR_ID['AG_COPY_REMOVE']}`, { props: { alt, src } })
      ]
      : [h(selector, tag)]
  }

  // render auto_link to vdom
  ['auto_link'] (h, cursor, block, token, outerClass) {
    const { start, end } = token.range
    const content = this.highlight(h, block, start, end, token)

    return [
      h('a', {
        props: {
          href: token.href
        }
      }, content)
    ]
  }

  // `a_link`: `<a href="url">anchor</a>`
  ['a_link'] (h, cursor, block, token, outerClass) {
    const className = this.getClassName(outerClass, block, token, cursor)
    const tagClassName = className === CLASS_OR_ID['AG_HIDE'] ? className : CLASS_OR_ID['AG_HTML_TAG']
    const { start, end } = token.range
    const openTag = this.highlight(h, block, start, start + token.openTag.length, token)
    const anchor = token.children.reduce((acc, to) => {
      const chunk = this[to.type](h, cursor, block, to, className)
      return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
    }, [])
    const closeTag = this.highlight(h, block, end - token.closeTag.length, end, token)

    return [
      h(`span.${tagClassName}.${CLASS_OR_ID['AG_OUTPUT_REMOVE']}`, openTag),
      h(`a.${CLASS_OR_ID['AG_A_LINK']}`, {
        dataset: {
          href: token.href
        }
      }, anchor),
      h(`span.${tagClassName}.${CLASS_OR_ID['AG_OUTPUT_REMOVE']}`, closeTag)
    ]
  }

  ['html_tag'] (h, cursor, block, token, outerClass) {
    const className = CLASS_OR_ID['AG_HTML_TAG']
    const { start, end } = token.range
    const tag = this.highlight(h, block, start, end, token)
    const isBr = /<br(?=\s|\/|>)/.test(token.tag)
    return [
      h(`span.${className}`, isBr ? [...tag, h('br')] : tag)
    ]
  }

  // 'link': /^(\[)((?:\[[^\]]*\]|[^\[\]]|\](?=[^\[]*\]))*?)(\\*)\]\((.*?)(\\*)\)/, // can nest
  link (h, cursor, block, token, outerClass) {
    const className = this.getClassName(outerClass, block, token, cursor)
    const linkClassName = className === CLASS_OR_ID['AG_HIDE'] ? className : CLASS_OR_ID['AG_LINK_IN_BRACKET']
    const { start, end } = token.range
    const firstMiddleBracket = this.highlight(h, block, start, start + 3, token)

    const firstBracket = this.highlight(h, block, start, start + 1, token)
    const middleBracket = this.highlight(
      h, block,
      start + 1 + token.anchor.length + token.backlash.first.length,
      start + 1 + token.anchor.length + token.backlash.first.length + 2,
      token
    )
    const hrefContent = this.highlight(
      h, block,
      start + 1 + token.anchor.length + token.backlash.first.length + 2,
      start + 1 + token.anchor.length + token.backlash.first.length + 2 + token.href.length,
      token
    )
    const middleHref = this.highlight(
      h, block, start + 1 + token.anchor.length + token.backlash.first.length,
      block, start + 1 + token.anchor.length + token.backlash.first.length + 2 + token.href.length,
      token
    )

    const lastBracket = this.highlight(h, block, end - 1, end, token)

    const firstBacklashStart = start + 1 + token.anchor.length
    const secondBacklashStart = end - 1 - token.backlash.second.length

    if (isLengthEven(token.backlash.first) && isLengthEven(token.backlash.second)) {
      if (!token.children.length && !token.backlash.first) { // no-text-link
        return [
          h(`span.${CLASS_OR_ID['AG_GRAY']}.${CLASS_OR_ID['AG_REMOVE']}`, firstMiddleBracket),
          h(`a.${CLASS_OR_ID['AG_NOTEXT_LINK']}`, {
            props: {
              href: token.href + encodeURI(token.backlash.second)
            }
          }, [
            ...hrefContent,
            ...this.backlashInToken(token.backlash.second, className, secondBacklashStart, token)
          ]),
          h(`span.${CLASS_OR_ID['AG_GRAY']}.${CLASS_OR_ID['AG_REMOVE']}`, lastBracket)
        ]
      } else { // has children
        return [
          h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, firstBracket),
          h('a', {
            dataset: {
              href: token.href + encodeURI(token.backlash.second)
            }
          }, [
            ...token.children.reduce((acc, to) => {
              const chunk = this[to.type](h, cursor, block, to, className)
              return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
            }, []),
            ...this.backlashInToken(token.backlash.first, className, firstBacklashStart, token)
          ]),
          h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, middleBracket),
          h(`span.${linkClassName}.${CLASS_OR_ID['AG_REMOVE']}`, [
            ...hrefContent,
            ...this.backlashInToken(token.backlash.second, className, secondBacklashStart, token)
          ]),
          h(`span.${className}.${CLASS_OR_ID['AG_REMOVE']}`, lastBracket)
        ]
      }
    } else {
      return [
        ...firstBracket,
        ...token.children.reduce((acc, to) => {
          const chunk = this[to.type](h, cursor, block, to, className)
          return Array.isArray(chunk) ? [...acc, ...chunk] : [...acc, chunk]
        }, []),
        ...this.backlashInToken(token.backlash.first, className, firstBacklashStart, token),
        ...middleHref,
        ...this.backlashInToken(token.backlash.second, className, secondBacklashStart, token),
        ...lastBracket
      ]
    }
  }

  del (h, cursor, block, token, outerClass) {
    return this.delEmStrongFac('del', h, cursor, block, token, outerClass)
  }

  em (h, cursor, block, token, outerClass) {
    return this.delEmStrongFac('em', h, cursor, block, token, outerClass)
  }

  strong (h, cursor, block, token, outerClass) {
    return this.delEmStrongFac('strong', h, cursor, block, token, outerClass)
  }
}

export default StateRender


import Vue from 'vue'
import LandingPage from '@/components/LandingPage'

describe('LandingPage.vue', () => {
  it('should render correct contents', () => {
    const vm = new Vue({
      el: document.createElement('div'),
      render: h => h(LandingPage)
    }).$mount()

    expect(vm.$el.querySelector('.title').textContent).to.contain('Welcome to your new project!')
  })
})


# Commented sections below can be used to run tests on the CI server
# https://simulatedgreg.gitbooks.io/electron-vue/content/en/testing.html#on-the-subject-of-ci-testing
osx_image: xcode9.2
sudo: required
dist: trusty

language: node_js
node_js:
  - 8

matrix:
  include:
  - os: osx
    env: CC=clang CXX=clang++ npm_config_clang=1
    compiler: clang
  - os: linux
    env: CC=clang CXX=clang++ npm_config_clang=1
    compiler: clang

cache:
  directories:
  - node_modules
  - "$HOME/.electron"
  - "$HOME/.cache"

addons:
  apt:
    packages:
    - libgnome-keyring-dev
    - icnsutils
    - graphicsmagick
    - xz-utils
    #- xvfb

before_install:
  #- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq update ; fi
  #- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then
  #    wget -qO - https://dl.winehq.org/wine-builds/Release.key | sudo apt-key add -;
  #    sudo apt-add-repository https://dl.winehq.org/wine-builds/ubuntu/;
  #  fi
  - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get -qq update ; fi
  #- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then sudo apt-get install --install-recommends winehq-stable ; fi

  #- if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then brew update ; fi

install:
  #- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then export DISPLAY=':99.0' ; fi
  #- if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then Xvfb :99 -screen 0 1024x768x24 > /dev/null 2>&1 & ; fi
  - curl -o- -L https://yarnpkg.com/install.sh | bash
  - source ~/.bashrc
  - npm install -g xvfb-maybe

  - $CC --version
  - $CXX --version
  - npm --version
  - yarn --version

  - yarn

script:
  #- xvfb-maybe node_modules/.bin/karma start test/unit/karma.conf.js
  #- yarn run pack && xvfb-maybe node_modules/.bin/mocha test/e2e
  - if [[ "$TRAVIS_OS_NAME" == "linux" ]]; then yarn run release:linux ; fi
  - if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then yarn run release ; fi

branches:
  only:
  - master


!macro customUnInstall
  MessageBox MB_YESNO "Do you want to delete user settings?" /SD IDNO IDNO SkipRemoval
    SetShellVarContext current
    RMDir /r "$APPDATA\marktext"
  SkipRemoval:
!macroend