///
/// utility functions for dealing with urls and http
///

var os = require('os');
var util = require('util');

var _ = require('underscore');
var Future = require('fibers/future');

var files = require('./files.js');
var auth = require('./auth.js');
var config = require('./config.js');
var release = require('./release.js');

// Compose a User-Agent header.
var getUserAgent = function () {
  var version;

  if (release.current)
    version = release.current.isCheckout() ? 'checkout' : release.current.name;
  else
    // This happens when we haven't finished starting up yet (say, the
    // user passed --release 1.2.3 and we have to download 1.2.3
    // before we can get going), or if we are using an installed copy
    // of Meteor to 'meteor update'ing a project that was created by a
    // checkout and doesn't have a version yet.
    version = files.inCheckout() ? 'checkout' : files.getToolsVersion();

  return util.format('Meteor/%s OS/%s (%s; %s; %s;)', version,
                     os.platform(), os.type(), os.release(), os.arch());
};

var httpHelpers = exports;
_.extend(exports, {
  getUserAgent: getUserAgent,

  // A wrapper around request with the following improvements:
  //
  // - It will respect proxy environment variables if present
  //   (HTTP_PROXY or HTTPS_PROXY as appropriate).
  //
  // - It will set a reasonable User-Agent header.
  //
  // - If you omit the callback it will run synchronously. The return
  //   value will be an object with keys 'response' and 'body' (with
  //   the same meaning as the arguments to request's normal
  //   callback), or it will throw.
  //
  // - If Set-Cookie headers are present on the response, *and* you
  //   are using a callback, it will parse the cookies and include
  //   them as a setCookie attribute on the object passed to the
  //   callback. setCookie is a simple map from cookie name to cookie
  //   value. If you want expiration time and attributes you'll have
  //   to parse it yourself. If there are multiple Set-Cookie headers
  //   for the same cookie it is unspecified which one you'll get.
  //
  // - You can provide a 'bodyStream' option which is a stream that
  //   will be used for the body of the request.
  //
  // - For authenticated MDG services, you can set the
  //   'useSessionHeader' and/or 'useAuthHeader' options (to true) to
  //   send X-Meteor-Session/X-Meteor-Auth headers using values from
  //   the session file.
  //
  // - forceSSL is always set to true. Always. And followRedirect is
  //   set to false since it doesn't understand origins (see comment
  //   in implementation).
  //
  // NB: With useSessionHeader and useAuthHeader, this function will
  // read *and possibly write to* the session file, so if you are
  // writing auth code (in auth.js) and you call it, be sure to reread
  // the session file afterwards.
  request: function (urlOrOptions, callback) {
    var options;
    if (!_.isObject(urlOrOptions))
      options = { url: urlOrOptions };
    else
      options = _.clone(urlOrOptions);

    var bodyStream;
    if (_.has(options, 'bodyStream')) {
      bodyStream = options.bodyStream;
      delete options.bodyStream;
    }

    options.headers = _.extend({
      'User-Agent': getUserAgent()
    }, options.headers || {});

    // This should never, ever be false, or else why are you using SSL?
    options.forceSSL = true;

    // followRedirect is very dangerous because request does not
    // appear to segregate cookies by origin, so any cookies (and
    // apparently headers as well, eg X-Meteor-Auth) sent on the
    // original request could get forwarded to an unexpected domain in
    // a redirect. This is almost certainly not something you ever
    // want.
    options.followRedirect = false;

    var useSessionHeader = options.useSessionHeader;
    delete options.useSessionHeader;
    var useAuthHeader = options.useAuthHeader;
    delete options.useAuthHeader;
    if (useSessionHeader || useAuthHeader) {
      var sessionHeader = auth.getSessionId(config.getAccountsDomain());
      if (sessionHeader)
        options.headers['X-Meteor-Session'] = sessionHeader;
      if (callback)
        throw new Error("session header can't be used with callback");
    }
    if (useAuthHeader) {
      var authHeader = auth.getSessionToken(config.getAccountsDomain());
      if (authHeader)
        options.headers['X-Meteor-Auth'] = authHeader;
    }

    var fut;
    if (! callback) {
      fut = new Future();
      callback = function (err, response, body) {
        if (err) {
          fut['throw'](err);
          return;
        }

        var setCookie = {};
        _.each(response.headers["set-cookie"] || [], function (h) {
          var match = h.match(/^([^=\s]+)=([^;\s]+)/);
          if (match)
            setCookie[match[1]] = match[2];
        });

        if (useSessionHeader && _.has(response.headers, "x-meteor-session")) {
          auth.setSessionId(config.getAccountsDomain(),
                            response.headers['x-meteor-session']);
        }

        fut['return']({
          response: response,
          body: body,
          setCookie: setCookie
        });
      };
    }

    // try to get proxy from environment
    var proxy = process.env.HTTP_PROXY || process.env.http_proxy || null;
    // if we're going to an https url, try the https_proxy env variable first.
    if (/^https/i.test(options.url)) {
      proxy = process.env.HTTPS_PROXY || process.env.https_proxy || proxy;
    }
    if (proxy && !options.proxy) {
      options.proxy = proxy;
    }

    // request is the most heavy-weight of the tool's npm dependencies; don't
    // require it until we definitely need it.
    var request = require('request');
    var req = request(options, callback);

    if (bodyStream)
      bodyStream.pipe(req);

    if (fut)
      return fut.wait();
    else
      return req;
  },

  // A synchronous wrapper around request(...) that returns the response "body"
  // or throws.
  //
  // (This has gone through a few refactors and it might be possible
  // to fully roll it into httpHelpers.request() at this point.)
  getUrl: function (urlOrOptions) {
    try {
      var result = httpHelpers.request(urlOrOptions);
    } catch (e) {
      throw new files.OfflineError(e);
    }

    var response = result.response;
    var body = result.body;

    if (response.statusCode >= 400 && response.statusCode < 600)
      throw body;
    else
      return body;
  }

});


summary: "Update the client when new client code is available",

keep keep replace keep keep keep keep keep

var selftest = require('../selftest.js');
var Sandbox = selftest.Sandbox;
var utils = require('../utils.js');
var Future = require('fibers/future');
var net = require('net');
var _ = require('underscore');
var files = require('../files.js');

selftest.define("css hot code push", function (options) {
  var s = new Sandbox({
    clients: options.clients,
  });

  s.createApp("myapp", "css-injection-test");
  s.cd("myapp");
  s.testWithAllClients(function (run) {
    run.baseTimeout = 20;
    run.match("myapp");
    run.match("proxy");
    run.match("MongoDB");
    run.match("running at");
    run.match("localhost");

    run.connectClient();

    run.waitSecs(20);
    run.match("client connected");

    // 'numCssChanges' variable is set to 0 on a client refresh.
    // Since CSS changes should not trigger a client refresh, numCssChanges
    // should never reset.

    // The css file is initially empty.
    run.match("numCssChanges: 0");

    // Some browsers represent no background as 'transparent', others use
    // rgba(0, 0, 0, 0).
    run.match(/background-color: (transparent|rgba\(0, 0, 0, 0\))/);

    // The server does NOT restart if a new css file is added.
    s.write("test.css", "body { background-color: red; }");
    run.match("Client modified -- refreshing");
    run.match("numCssChanges: 1");
    run.match("background-color: rgb(255, 0, 0)");

    s.write("test.css", "body { background-color: blue; }");
    run.match("Client modified -- refreshing");
    run.match("numCssChanges: 2");
    run.match("background-color: rgb(0, 0, 255)");

    // The server does NOT restart if a css file is removed.
    s.unlink("test.css");
    run.match("Client modified -- refreshing");
    run.match("numCssChanges: 3");
    run.match(/background-color: (transparent|rgba\(0, 0, 0, 0\))/);
    run.stop();
  });
});

selftest.define("versioning hot code push", function (options) {
  var s = new Sandbox();

  s.set("AUTOUPDATE_VERSION", "1.0");
  s.createApp("myapp", "hot-code-push-test");
  s.cd("myapp");

  s.testWithAllClients(function (run) {
    run.baseTimeout = 20;
    run.match("myapp");
    run.match("proxy");
    run.match("MongoDB");
    run.match("running at");
    run.match("localhost");
    run.connectClient();
    run.waitSecs(20);

    run.match("client connected: 0");

    run.forbidAll("Error listening");

    run.stop();
  });
});

selftest.define("javascript hot code push", function (options) {
  var s = new Sandbox({
    clients: options.clients,
  });

  s.createApp("myapp", "hot-code-push-test");
  s.cd("myapp");
  s.testWithAllClients(function (run) {
    run.baseTimeout = 20;
    run.match("myapp");
    run.match("proxy");
    run.match("MongoDB");
    run.match("running at");
    run.match("localhost");

    run.connectClient();
    run.waitSecs(20);

    // There is initially no JavaScript file.
    run.match("client connected: 0");
    run.match("jsVar: undefined");

    // The server and client both restart if a shared js file is added
    // or removed.
    s.write("test.js", "jsVar = 'foo'");
    run.match("server restarted");
    run.match("client connected: 0");
    run.match("jsVar: foo");

    s.unlink("test.js");
    run.match("server restarted");
    run.match("client connected: 0");
    run.match("jsVar: undefined");

    // Only the client should refresh if a client js file is added. Thus,
    // "client connected" variable will be incremented.
    s.mkdir("client");
    s.write("client/test.js", "jsVar = 'bar'");
    run.match("client connected: 1");
    run.match("jsVar: bar");

    s.unlink("client/test.js");
    run.match("client connected: 2");
    run.match("jsVar: undefined");

    // When we change a server file the client should not refresh. We observe
    // this by changing a server file and then a client file and verifying
    // that the client has only connected once.
    s.mkdir("server");
    s.write("server/test.js", "jsVar = 'bar'");
    run.match("server restarted");

    // Setting the autoupdateVersion to a different string should also
    // force the client to restart.
    s.write("server/test.js",
            "Package.autoupdate.Autoupdate.autoupdateVersion = 'random'");
    run.match("server restarted");
    run.match("client connected: 0");
    run.match("jsVar: undefined");

    s.unlink("server/test.js");
    run.match("server restarted");

    s.write("client/empty.js", "");
    run.match("client connected: 0");
    // We should not be able to access a server variable from the client.
    run.match("jsVar: undefined");

    s.unlink("client/empty.js");
    run.match("client connected: 1");
    run.match("jsVar: undefined");

    // Break the HTML file. This should kill the server, and print errors.
    // (It would be reasonable behavior for this to NOT kill the server, since
    // it only affects the client. But this is a regression test for a bug where
    // fixing the HTML file wouldn't actually restart the server; that's the
    // important part of this test.)
    s.write("hot-code-push-test.html", ">");
    run.match("Errors prevented startup");
    run.match("bad formatting in HTML template");
    // Fix it. It should notice, and restart. The client will restart too.
    s.write("hot-code-push-test.html", "");
    run.match("server restarted");
    run.match("client connected: 0");
    // Write something else to it. The client should restart.
    s.write("hot-code-push-test.html", "<head><title>foo</title></head>");
    run.match("Client modified -- refreshing");
    run.match("client connected: 1");
    run.match("jsVar: undefined");

    s.write("client/test.js", "jsVar = 'bar'");
    run.match("client connected: 2");
    run.match("jsVar: bar");

    s.write("client/test.js", "jsVar = 'baz'");
    run.match("client connected: 3");
    run.match("jsVar: baz");

    s.unlink("client/test.js");

    run.stop();
  });
});


MeteorDeveloperAccounts = {};

MeteorDeveloperAccounts._server = "https://www.meteor.com";

// Options are:
//  - developerAccountsServer: defaults to "https://www.meteor.com"
MeteorDeveloperAccounts._config = function (options) {
  if (options.developerAccountsServer) {
    MeteorDeveloperAccounts._server = options.developerAccountsServer;
  }
};


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

Package.on_use(function (api) {
  api.use(['underscore', 'deps', 'ejson']);
  // If we are loading mongo-livedata, let you store ObjectIDs in it.
  api.use('mongo-livedata', {weak: true});
  api.export('ReactiveDict');
  api.add_files('reactive-dict.js');
});

Package.on_test(function (api) {
  api.use('tinytest');
});


Accounts.oauth.registerService('github');

if (Meteor.isClient) {
  Meteor.loginWithGithub = function(options, callback) {
    // support a callback without options
    if (! callback && typeof options === "function") {
      callback = options;
      options = null;
    }

    var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
    Github.requestCredential(options, credentialRequestCompleteCallback);
  };
} else {
  Accounts.addAutopublishFields({
    // not sure whether the github api can be used from the browser,
    // thus not sure if we should be sending access tokens; but we do it
    // for all other oauth2 providers, and it may come in handy.
    forLoggedInUser: ['services.github'],
    forOtherUsers: ['services.github.username']
  });
}


s.write("server/test.js", "jsVar = 'bar'");

// Setting the autoupdateVersion to a different string should also

// We should not be able to access a server variable from the client.

keep keep keep keep replace replace replace replace replace replace replace replace replace replace replace keep keep keep keep


// _assign is like _.extend or the upcoming Object.assign.
// Copy src's own, enumerable properties onto tgt and return
// tgt.
var _hasOwnProperty = Object.prototype.hasOwnProperty;
var _assign = function (tgt, src) {
  for (var k in src) {
    if (_hasOwnProperty.call(src, k))
      tgt[k] = src[k];
  }
  return tgt;
};

HTMLTools.TemplateTag = function (props) {
  if (! (this instanceof HTMLTools.TemplateTag))
    // called without `new`
    return new HTMLTools.TemplateTag;

  if (props)
    _assign(this, props);
};

_assign(HTMLTools.TemplateTag.prototype, {
  constructorName: 'HTMLTools.TemplateTag',
  toJS: function (visitor) {
    return visitor.generateCall(this.constructorName,
                                _assign({}, this));
  }
});


Package.describe({
  summary: "contains a plugin",
  version: "1.1.0"
});

Package._transitional_registerBuildPlugin({
  name: "myPlugin",
  use: [],
  sources: [
    'plugin/plugin.js'
  ],
});


// Break the HTML file. This should kill the server, and print errors.

// (It would be reasonable behavior for this to NOT kill the server, since

keep keep keep keep add keep keep keep keep keep

// Subscribe to the `meteor_autoupdate_clientVersions` collection,
// which contains the set of acceptable client versions.
//
// A "hard code push" occurs when the running client version is not in
// the set of acceptable client versions (or the server updates the
// collection, there is a published client version marked `current` and
// the running client version is no longer in the set).
//
// When the `reload` package is loaded, a hard code push causes
// the browser to reload, so that it will load the latest client
// version from the server.
//
// A "soft code push" represents the situation when the running client
// version is in the set of acceptable versions, but there is a newer
// version available on the server.
//
// `Autoupdate.newClientAvailable` is a reactive data source which
// becomes `true` if there is a new version of the client is available on
// the server.
//
// This package doesn't implement a soft code reload process itself,
// but `newClientAvailable` could be used for example to display a
// "click to reload" link to the user.

// The client version of the client code currently running in the
// browser.
var autoupdateVersion = __meteor_runtime_config__.autoupdateVersion || "unknown";
var autoupdateVersionRefreshable =
  __meteor_runtime_config__.autoupdateVersionRefreshable || "unknown";

// The collection of acceptable client versions.
ClientVersions = new Meteor.Collection("meteor_autoupdate_clientVersions");

Autoupdate = {};

Autoupdate.newClientAvailable = function () {
  return !! ClientVersions.findOne({
               refreshable: false,
               version: {$ne: autoupdateVersion} }) ||
         !! ClientVersions.findOne({
               refreshable: true,
               version: {$ne: autoupdateVersionRefreshable} });
};

var knownToSupportCssOnLoad = false;

var retry = new Retry({
  // Unlike the stream reconnect use of Retry, which we want to be instant
  // in normal operation, this is a wacky failure. We don't want to retry
  // right away, we can start slowly.
  //
  // A better way than timeconstants here might be to use the knowledge
  // of when we reconnect to help trigger these retries. Typically, the
  // server fixing code will result in a restart and reconnect, but
  // potentially the subscription could have a transient error.
  minCount: 0, // don't do any immediate retries
  baseTimeout: 30*1000 // start with 30s
});
var failures = 0;

Autoupdate._retrySubscription = function () {
  Meteor.subscribe("meteor_autoupdate_clientVersions", {
    onError: function (error) {
      Meteor._debug("autoupdate subscription failed:", error);
      failures++;
      retry.retryLater(failures, function () {
        // Just retry making the subscription, don't reload the whole
        // page. While reloading would catch more cases (for example,
        // the server went back a version and is now doing old-style hot
        // code push), it would also be more prone to reload loops,
        // which look really bad to the user. Just retrying the
        // subscription over DDP means it is at least possible to fix by
        // updating the server.
        Autoupdate._retrySubscription();
      });
    },
    onReady: function () {
      if (Package.reload) {
        var checkNewVersionDocument = function (id, fields) {
          var self = this;
          var isRefreshable = id === 'version-refreshable';
          if (isRefreshable &&
              fields.version !== autoupdateVersionRefreshable) {
            autoupdateVersionRefreshable = fields.version;
            // Switch out old css links for the new css links. Inspired by:
            // https://github.com/guard/guard-livereload/blob/master/js/livereload.js#L710
            var newCss = fields.assets.allCss;
            var oldLinks = [];
            _.each(document.getElementsByTagName('link'), function (link) {
              if (link.className === '__meteor-css__') {
                oldLinks.push(link);
              }
            });

            var waitUntilCssLoads = function  (link, callback) {
              var executeCallback = _.once(callback);
              link.onload = function () {
                knownToSupportCssOnLoad = true;
                executeCallback();
              };
              if (! knownToSupportCssOnLoad) {
                var id = Meteor.setInterval(function () {
                  if (link.sheet) {
                    executeCallback();
                    Meteor.clearInterval(id);
                  }
                }, 50);
              }
            };

            var attachStylesheetLink = function (newLink) {
              var removeOldLinks = _.after(newCss.length, function () {
                _.each(oldLinks, function (oldLink) {
                  oldLink.parentNode.removeChild(oldLink);
                });
              });

              document.getElementsByTagName("head").item(0).appendChild(newLink);

              waitUntilCssLoads(newLink, function () {
                Meteor.setTimeout(removeOldLinks, 200);
              });
            };

            _.each(newCss, function (css) {
              var newLink = document.createElement("link");
              newLink.setAttribute("rel", "stylesheet");
              newLink.setAttribute("type", "text/css");
              newLink.setAttribute("class", "__meteor-css__");
              newLink.setAttribute("href", css.url);
              attachStylesheetLink(newLink);
            });
          }
          else if (! isRefreshable &&
                   fields.version !== autoupdateVersionRefreshable && handle) {
            handle.stop();
            Package.reload.Reload._reload();
          }
        };

        var handle = ClientVersions.find().observeChanges({
          added: checkNewVersionDocument,
          changed: checkNewVersionDocument
        });
      }
    }
  });
};
Autoupdate._retrySubscription();


StubStream = function () {
  var self = this;

  self.sent = [];
  self.callbacks = {};
};

_.extend(StubStream.prototype, {
  // Methods from Stream
  on: function (name, callback) {
    var self = this;

    if (!self.callbacks[name])
      self.callbacks[name] = [callback];
    else
      self.callbacks[name].push(callback);
  },

  send: function (data) {
    var self = this;
    self.sent.push(data);
  },

  status: function () {
    return {status: "connected", fake: true};
  },

  reconnect: function () {
    // no-op
  },

  _lostConnection: function () {
    // no-op
  },

  // Methods for tests
  receive: function (data) {
    var self = this;

    if (typeof data === 'object') {
      data = EJSON.stringify(data);
    }

    _.each(self.callbacks['message'], function (cb) {
      cb(data);
    });
  },

  reset: function () {
    var self = this;
    _.each(self.callbacks['reset'], function (cb) {
      cb();
    });
  },

  // Provide a tag to detect stub streams.
  // We don't log heartbeat failures on stub streams, for example.
  _isStub: true
});


Accounts.oauth = {};

var services = {};

// Helper for registering OAuth based accounts packages.
// On the server, adds an index to the user collection.
Accounts.oauth.registerService = function (name) {
  if (_.has(services, name))
    throw new Error("Duplicate service: " + name);
  services[name] = true;

  if (Meteor.server) {
    // Accounts.updateOrCreateUserFromExternalService does a lookup by this id,
    // so this should be a unique index. You might want to add indexes for other
    // fields returned by your service (eg services.github.login) but you can do
    // that in your app.
    Meteor.users._ensureIndex('services.' + name + '.id',
                              {unique: 1, sparse: 1});
  }
};

Accounts.oauth.serviceNames = function () {
  return _.keys(services);
};


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.


// Setting the autoupdateVersion to a different string should also


// Give a sort spec, which can be in any of these forms:
//   {"key1": 1, "key2": -1}
//   [["key1", "asc"], ["key2", "desc"]]
//   ["key1", ["key2", "desc"]]
//
// (.. with the first form being dependent on the key enumeration
// behavior of your javascript VM, which usually does what you mean in
// this case if the key names don't look like integers ..)
//
// return a function that takes two objects, and returns -1 if the
// first object comes first in order, 1 if the second object comes
// first, or 0 if neither object comes before the other.

Minimongo.Sorter = function (spec, options) {
  var self = this;
  options = options || {};

  self._sortSpecParts = [];

  var addSpecPart = function (path, ascending) {
    if (!path)
      throw Error("sort keys must be non-empty");
    if (path.charAt(0) === '$')
      throw Error("unsupported sort key: " + path);
    self._sortSpecParts.push({
      path: path,
      lookup: makeLookupFunction(path),
      ascending: ascending
    });
  };

  if (spec instanceof Array) {
    for (var i = 0; i < spec.length; i++) {
      if (typeof spec[i] === "string") {
        addSpecPart(spec[i], true);
      } else {
        addSpecPart(spec[i][0], spec[i][1] !== "desc");
      }
    }
  } else if (typeof spec === "object") {
    _.each(spec, function (value, key) {
      addSpecPart(key, value >= 0);
    });
  } else {
    throw Error("Bad sort specification: " + JSON.stringify(spec));
  }

  // To implement affectedByModifier, we piggy-back on top of Matcher's
  // affectedByModifier code; we create a selector that is affected by the same
  // modifiers as this sort order. This is only implemented on the server.
  if (self.affectedByModifier) {
    var selector = {};
    _.each(self._sortSpecParts, function (spec) {
      selector[spec.path] = 1;
    });
    self._selectorForAffectedByModifier = new Minimongo.Matcher(selector);
  }

  self._keyComparator = composeComparators(
    _.map(self._sortSpecParts, function (spec, i) {
      return self._keyFieldComparator(i);
    }));

  // If you specify a matcher for this Sorter, _keyFilter may be set to a
  // function which selects whether or not a given "sort key" (tuple of values
  // for the different sort spec fields) is compatible with the selector.
  self._keyFilter = null;
  options.matcher && self._useWithMatcher(options.matcher);
};

// In addition to these methods, sorter_project.js defines combineIntoProjection
// on the server only.
_.extend(Minimongo.Sorter.prototype, {
  getComparator: function (options) {
    var self = this;

    // If we have no distances, just use the comparator from the source
    // specification (which defaults to "everything is equal".
    if (!options || !options.distances) {
      return self._getBaseComparator();
    }

    var distances = options.distances;

    // Return a comparator which first tries the sort specification, and if that
    // says "it's equal", breaks ties using $near distances.
    return composeComparators([self._getBaseComparator(), function (a, b) {
      if (!distances.has(a._id))
        throw Error("Missing distance for " + a._id);
      if (!distances.has(b._id))
        throw Error("Missing distance for " + b._id);
      return distances.get(a._id) - distances.get(b._id);
    }]);
  },

  _getPaths: function () {
    var self = this;
    return _.pluck(self._sortSpecParts, 'path');
  },

  // Finds the minimum key from the doc, according to the sort specs.  (We say
  // "minimum" here but this is with respect to the sort spec, so "descending"
  // sort fields mean we're finding the max for that field.)
  //
  // Note that this is NOT "find the minimum value of the first field, the
  // minimum value of the second field, etc"... it's "choose the
  // lexicographically minimum value of the key vector, allowing only keys which
  // you can find along the same paths".  ie, for a doc {a: [{x: 0, y: 5}, {x:
  // 1, y: 3}]} with sort spec {'a.x': 1, 'a.y': 1}, the only keys are [0,5] and
  // [1,3], and the minimum key is [0,5]; notably, [0,3] is NOT a key.
  _getMinKeyFromDoc: function (doc) {
    var self = this;
    var minKey = null;

    self._generateKeysFromDoc(doc, function (key) {
      if (!self._keyCompatibleWithSelector(key))
        return;

      if (minKey === null) {
        minKey = key;
        return;
      }
      if (self._compareKeys(key, minKey) < 0) {
        minKey = key;
      }
    });

    // This could happen if our key filter somehow filters out all the keys even
    // though somehow the selector matches.
    if (minKey === null)
      throw Error("sort selector found no keys in doc?");
    return minKey;
  },

  _keyCompatibleWithSelector: function (key) {
    var self = this;
    return !self._keyFilter || self._keyFilter(key);
  },

  // Iterates over each possible "key" from doc (ie, over each branch), calling
  // 'cb' with the key.
  _generateKeysFromDoc: function (doc, cb) {
    var self = this;

    if (self._sortSpecParts.length === 0)
      throw new Error("can't generate keys without a spec");

    // maps index -> ({'' -> value} or {path -> value})
    var valuesByIndexAndPath = [];

    var pathFromIndices = function (indices) {
      return indices.join(',') + ',';
    };

    var knownPaths = null;

    _.each(self._sortSpecParts, function (spec, whichField) {
      // Expand any leaf arrays that we find, and ignore those arrays
      // themselves.  (We never sort based on an array itself.)
      var branches = expandArraysInBranches(spec.lookup(doc), true);

      // If there are no values for a key (eg, key goes to an empty array),
      // pretend we found one null value.
      if (!branches.length)
        branches = [{value: null}];

      var usedPaths = false;
      valuesByIndexAndPath[whichField] = {};
      _.each(branches, function (branch) {
        if (!branch.arrayIndices) {
          // If there are no array indices for a branch, then it must be the
          // only branch, because the only thing that produces multiple branches
          // is the use of arrays.
          if (branches.length > 1)
            throw Error("multiple branches but no array used?");
          valuesByIndexAndPath[whichField][''] = branch.value;
          return;
        }

        usedPaths = true;
        var path = pathFromIndices(branch.arrayIndices);
        if (_.has(valuesByIndexAndPath[whichField], path))
          throw Error("duplicate path: " + path);
        valuesByIndexAndPath[whichField][path] = branch.value;

        // If two sort fields both go into arrays, they have to go into the
        // exact same arrays and we have to find the same paths.  This is
        // roughly the same condition that makes MongoDB throw this strange
        // error message.  eg, the main thing is that if sort spec is {a: 1,
        // b:1} then a and b cannot both be arrays.
        //
        // (In MongoDB it seems to be OK to have {a: 1, 'a.x.y': 1} where 'a'
        // and 'a.x.y' are both arrays, but we don't allow this for now.
        // #NestedArraySort
        // XXX achieve full compatibility here
        if (knownPaths && !_.has(knownPaths, path)) {
          throw Error("cannot index parallel arrays");
        }
      });

      if (knownPaths) {
        // Similarly to above, paths must match everywhere, unless this is a
        // non-array field.
        if (!_.has(valuesByIndexAndPath[whichField], '') &&
            _.size(knownPaths) !== _.size(valuesByIndexAndPath[whichField])) {
          throw Error("cannot index parallel arrays!");
        }
      } else if (usedPaths) {
        knownPaths = {};
        _.each(valuesByIndexAndPath[whichField], function (x, path) {
          knownPaths[path] = true;
        });
      }
    });

    if (!knownPaths) {
      // Easy case: no use of arrays.
      var soleKey = _.map(valuesByIndexAndPath, function (values) {
        if (!_.has(values, ''))
          throw Error("no value in sole key case?");
        return values[''];
      });
      cb(soleKey);
      return;
    }

    _.each(knownPaths, function (x, path) {
      var key = _.map(valuesByIndexAndPath, function (values) {
        if (_.has(values, ''))
          return values[''];
        if (!_.has(values, path))
          throw Error("missing path?");
        return values[path];
      });
      cb(key);
    });
  },

  // Takes in two keys: arrays whose lengths match the number of spec
  // parts. Returns negative, 0, or positive based on using the sort spec to
  // compare fields.
  _compareKeys: function (key1, key2) {
    var self = this;
    if (key1.length !== self._sortSpecParts.length ||
        key2.length !== self._sortSpecParts.length) {
      throw Error("Key has wrong length");
    }

    return self._keyComparator(key1, key2);
  },

  // Given an index 'i', returns a comparator that compares two key arrays based
  // on field 'i'.
  _keyFieldComparator: function (i) {
    var self = this;
    var invert = !self._sortSpecParts[i].ascending;
    return function (key1, key2) {
      var compare = LocalCollection._f._cmp(key1[i], key2[i]);
      if (invert)
        compare = -compare;
      return compare;
    };
  },

  // Returns a comparator that represents the sort specification (but not
  // including a possible geoquery distance tie-breaker).
  _getBaseComparator: function () {
    var self = this;

    // If we're only sorting on geoquery distance and no specs, just say
    // everything is equal.
    if (!self._sortSpecParts.length) {
      return function (doc1, doc2) {
        return 0;
      };
    }

    return function (doc1, doc2) {
      var key1 = self._getMinKeyFromDoc(doc1);
      var key2 = self._getMinKeyFromDoc(doc2);
      return self._compareKeys(key1, key2);
    };
  },

  // In MongoDB, if you have documents
  //    {_id: 'x', a: [1, 10]} and
  //    {_id: 'y', a: [5, 15]},
  // then C.find({}, {sort: {a: 1}}) puts x before y (1 comes before 5).
  // But  C.find({a: {$gt: 3}}, {sort: {a: 1}}) puts y before x (1 does not
  // match the selector, and 5 comes before 10).
  //
  // The way this works is pretty subtle!  For example, if the documents
  // are instead {_id: 'x', a: [{x: 1}, {x: 10}]}) and
  //             {_id: 'y', a: [{x: 5}, {x: 15}]}),
  // then C.find({'a.x': {$gt: 3}}, {sort: {'a.x': 1}}) and
  //      C.find({a: {$elemMatch: {x: {$gt: 3}}}}, {sort: {'a.x': 1}})
  // both follow this rule (y before x).  (ie, you do have to apply this
  // through $elemMatch.)
  //
  // So if you pass a matcher to this sorter's constructor, we will attempt to
  // skip sort keys that don't match the selector. The logic here is pretty
  // subtle and undocumented; we've gotten as close as we can figure out based
  // on our understanding of Mongo's behavior.
  _useWithMatcher: function (matcher) {
    var self = this;

    if (self._keyFilter)
      throw Error("called _useWithMatcher twice?");

    // If we are only sorting by distance, then we're not going to bother to
    // build a key filter.
    // XXX figure out how geoqueries interact with this stuff
    if (_.isEmpty(self._sortSpecParts))
      return;

    var selector = matcher._selector;

    // If the user just passed a literal function to find(), then we can't get a
    // key filter from it.
    if (selector instanceof Function)
      return;

    var constraintsByPath = {};
    _.each(self._sortSpecParts, function (spec, i) {
      constraintsByPath[spec.path] = [];
    });

    _.each(selector, function (subSelector, key) {
      // XXX support $and and $or

      var constraints = constraintsByPath[key];
      if (!constraints)
        return;

      // XXX it looks like the real MongoDB implementation isn't "does the
      // regexp match" but "does the value fall into a range named by the
      // literal prefix of the regexp", ie "foo" in /^foo(bar|baz)+/  But
      // "does the regexp match" is a good approximation.
      if (subSelector instanceof RegExp) {
        // As far as we can tell, using either of the options that both we and
        // MongoDB support ('i' and 'm') disables use of the key filter. This
        // makes sense: MongoDB mostly appears to be calculating ranges of an
        // index to use, which means it only cares about regexps that match
        // one range (with a literal prefix), and both 'i' and 'm' prevent the
        // literal prefix of the regexp from actually meaning one range.
        if (subSelector.ignoreCase || subSelector.multiline)
          return;
        constraints.push(regexpElementMatcher(subSelector));
        return;
      }

      if (isOperatorObject(subSelector)) {
        _.each(subSelector, function (operand, operator) {
          if (_.contains(['$lt', '$lte', '$gt', '$gte'], operator)) {
            // XXX this depends on us knowing that these operators don't use any
            // of the arguments to compileElementSelector other than operand.
            constraints.push(
              ELEMENT_OPERATORS[operator].compileElementSelector(operand));
          }

          // See comments in the RegExp block above.
          if (operator === '$regex' && !subSelector.$options) {
            constraints.push(
              ELEMENT_OPERATORS.$regex.compileElementSelector(
                operand, subSelector));
          }

          // XXX support {$exists: true}, $mod, $type, $in, $elemMatch
        });
        return;
      }

      // OK, it's an equality thing.
      constraints.push(equalityElementMatcher(subSelector));
    });

    // It appears that the first sort field is treated differently from the
    // others; we shouldn't create a key filter unless the first sort field is
    // restricted, though after that point we can restrict the other sort fields
    // or not as we wish.
    if (_.isEmpty(constraintsByPath[self._sortSpecParts[0].path]))
      return;

    self._keyFilter = function (key) {
      return _.all(self._sortSpecParts, function (specPart, index) {
        return _.all(constraintsByPath[specPart.path], function (f) {
          return f(key[index]);
        });
      });
    };
  }
});

// Given an array of comparators
// (functions (a,b)->(negative or positive or zero)), returns a single
// comparator which uses each comparator in order and returns the first
// non-zero value.
var composeComparators = function (comparatorArray) {
  return function (a, b) {
    for (var i = 0; i < comparatorArray.length; ++i) {
      var compare = comparatorArray[i](a, b);
      if (compare !== 0)
        return compare;
    }
    return 0;
  };
};


// Global flag for phantomjs (or other browser) to eval to see if we're done.
DONE = false;
// Failure count for phantomjs exit code
FAILURES = null;

TEST_STATUS = {
  DONE: false,
  FAILURES: null
};

// xUnit format uses XML output
var XML_CHAR_MAP = {
  '<': '<',
  '>': '>',
  '&': '&',
  '"': '"',
  "'": '''
};

// Escapes a string for insertion into XML
var escapeXml = function (s) {
  return s.replace(/[<>&"']/g, function (c) {
    return XML_CHAR_MAP[c];
  });
}

// Returns a human name for a test
var getName = function (result) {
  return (result.server ? "S: " : "C: ") +
    result.groupPath.join(" - ") + " - " + result.test;
};

// Calls console.log, but returns silently if console.log is not available
var log = function (/*arguments*/) {
  if (typeof console !== 'undefined') {
    console.log.apply(console, arguments);
  }
};

// Logs xUnit output, if xunit output is enabled
// Output is sent to console.log, prefixed with a magic string 'XUNIT '
// By grepping for that prefix, the xUnit output can be extracted
var xunit = function (s) {
  if (xunitEnabled) {
    log('XUNIT ' + s);
  }
};

var passed = 0;
var failed = 0;
var expected = 0;
var resultSet = {};
var toReport = [];

var hrefPath = document.location.href.split("/");
var platform = decodeURIComponent(hrefPath.length && hrefPath[hrefPath.length - 1]);
if (!platform)
  platform = "local";

// We enable xUnit output when platform is xunit
var xunitEnabled = (platform == 'xunit');

var doReport = Meteor &&
      Meteor.settings &&
      Meteor.settings.public &&
      Meteor.settings.public.runId;
var report = function (name, last) {
  if (doReport) {
    var data = {
      run_id: Meteor.settings.public.runId,
      testPath: resultSet[name].testPath,
      status: resultSet[name].status,
      platform: platform,
      server: resultSet[name].server,
      fullName: name.substr(3)
    };
    if ((data.status === "FAIL" || data.status === "EXPECTED") &&
        !_.isEmpty(resultSet[name].events)) {
      // only send events when bad things happen
      data.events = resultSet[name].events;
    }
    if (last)
      data.end = new Date();
    else
      data.start = new Date();
    toReport.push(EJSON.toJSONValue(data));
  }
};
var sendReports = function (callback) {
  var reports = toReport;
  if (!callback)
    callback = function () {};
  toReport = [];
  if (doReport)
    Meteor.call("report", reports, callback);
  else
    callback();
};
Meteor.startup(function () {
  setTimeout(sendReports, 500);
  setInterval(sendReports, 2000);

  Tinytest._runTestsEverywhere(
    function (results) {
      var name = getName(results);
      if (!_.has(resultSet, name)) {
        var testPath = EJSON.clone(results.groupPath);
        testPath.push(results.test);
        resultSet[name] = {
          name: name,
          status: "PENDING",
          events: [],
          server: !!results.server,
          testPath: testPath,
          test: results.test
        };
        report(name, false);
      }
      // Loop through events, and record status for each test
      // Also log result if test has finished
      _.each(results.events, function (event) {
        resultSet[name].events.push(event);
        switch (event.type) {
        case "ok":
          break;
        case "expected_fail":
          if (resultSet[name].status !== "FAIL")
            resultSet[name].status = "EXPECTED";
          break;
        case "exception":
          log(name, ":", "!!!!!!!!! FAIL !!!!!!!!!!!");
          if (event.details && event.details.stack)
            log(event.details.stack);
          else
            log("Test failed with exception");
          failed++;
          break;
        case "finish":
          switch (resultSet[name].status) {
          case "OK":
            break;
          case "PENDING":
            resultSet[name].status = "OK";
            report(name, true);
            log(name, ":", "OK");
            passed++;
            break;
          case "EXPECTED":
            report(name, true);
            log(name, ":", "EXPECTED FAILURE");
            expected++;
            break;
          case "FAIL":
            failed++;
            report(name, true);
            log(name, ":", "!!!!!!!!! FAIL !!!!!!!!!!!");
            log(JSON.stringify(resultSet[name].info));
            break;
          default:
            log(name, ": unknown state for the test to be in");
          }
          break;
        default:
          resultSet[name].status = "FAIL";
          resultSet[name].info = results;
          break;
        }
      });
    },

    // After test completion, log a quick summary
    function () {
      if (failed > 0) {
        log("~~~~~~~ THERE ARE FAILURES ~~~~~~~");
      }
      log("passed/expected/failed/total", passed, "/", expected, "/", failed, "/", _.size(resultSet));
      sendReports(function () {
        if (doReport) {
          log("Waiting 3s for any last reports to get sent out");
          setTimeout(function () {
            TEST_STATUS.FAILURES = FAILURES = failed;
            TEST_STATUS.DONE = DONE = true;
          }, 3000);
        } else {
          TEST_STATUS.FAILURES = FAILURES = failed;
          TEST_STATUS.DONE = DONE = true;
        }
      });

      // Also log xUnit output
      xunit('<testsuite errors="" failures="" name="meteor" skips="" tests="" time="">');
      _.each(resultSet, function (result, name) {
        var classname = result.testPath.join('.').replace(/ /g, '-') + (result.server ? "-server" : "-client");
        var name = result.test.replace(/ /g, '-') + (result.server ? "-server" : "-client");
        var time = "";
        var error = "";
        _.each(result.events, function (event) {
          switch (event.type) {
            case "finish":
              var timeMs = event.timeMs;
              if (timeMs !== undefined) {
                time = (timeMs / 1000) + "";
              }
              break;
            case "fail":
              var details = event.details || {};
              error = (details.message || '?') + " filename=" + (details.filename || '?') + " line=" + (details.line || '?');
          }
        });
        switch (event.status) {
          case "FAIL":
            error = error || '?';
            break;
          case "EXPECTED":
            error = "Expected failure";
            break;
        }

        xunit('<testcase classname="' + escapeXml(classname) + '" name="' + escapeXml(name) + '" time="' + time + '">');
        if (error) {
          xunit('  <failure message="test failure">' + escapeXml(error) + '</failure>');
        }
        xunit('</testcase>');
      });
      xunit('</testsuite>');
    },
    ["tinytest"]);
});


keep keep keep keep keep replace keep keep keep keep keep

import selection from '../selection'
import { tokenizer } from '../parser/parse'
import { conflict } from '../utils'
import { getTextContent } from '../utils/domManipulate'
import { CLASS_OR_ID } from '../config'

const INLINE_UPDATE_FREGMENTS = [
  '^([*+-]\\s)', // Bullet list
  '^(\\[[x\\s]{1}\\]\\s)', // Task list
  '^(\\d+\\.\\s)', // Order list
  '^\\s{0,3}(#{1,6})(?=\\s{1,}|$)', // ATX heading
  '^(>).+', // Block quote
  '^\\s{0,3}((?:\\*\\s*\\*\\s*\\*|-\\s*-\\s*-|_\\s*_\\s*_)[\\s\\*\\-\\_]*)$' // Thematic break
]

const INLINE_UPDATE_REG = new RegExp(INLINE_UPDATE_FREGMENTS.join('|'), 'i')

let lastCursor = null

const updateCtrl = ContentState => {
  ContentState.prototype.checkNeedRender = function (block) {
    const { start: cStart, end: cEnd } = this.cursor
    const startOffset = cStart.offset
    const endOffset = cEnd.offset
    const tokens = tokenizer(block.text)
    const textLen = block.text.length

    for (const token of tokens) {
      if (token.type === 'text') continue
      const { start, end } = token.range
      if (
        conflict([Math.max(0, start - 1), Math.min(textLen, end + 1)], [startOffset, startOffset]) ||
        conflict([Math.max(0, start - 1), Math.min(textLen, end + 1)], [endOffset, endOffset])
      ) {
        return true
      }
    }
    return false
  }

  ContentState.prototype.checkInlineUpdate = function (block) {
    if (/th|td|figure/.test(block.type)) return false
    const { text } = block
    const parent = this.getParent(block)
    const [match, bullet, tasklist, order, header, blockquote, hr] = text.match(INLINE_UPDATE_REG) || []
    let newType

    switch (true) {
      case (hr && new Set(hr.split('').filter(i => /\S/.test(i))).size === 1):
        this.updateHr(block, hr)
        return true

      case !!bullet:
        this.updateList(block, 'bullet', bullet)
        return true

      case !!tasklist && parent && parent.listItemType === 'bullet': // only `bullet` list item can be update to `task` list item
        this.updateTaskListItem(block, 'tasklist', tasklist)
        return true

      case !!order:
        this.updateList(block, 'order', order)
        return true

      case !!header:
        newType = `h${header.length}`
        if (block.type !== newType) {
          block.type = newType // updateHeader
          return true
        }
        break

      case !!blockquote:
        this.updateBlockQuote(block)
        return true

      case !match:
      default:
        newType = 'p'
        if (block.type !== newType) {
          block.type = newType // updateP
          return true
        }
        break
    }

    return false
  }

  ContentState.prototype.updateTaskListItem = function (block, type, marker = '') {
    const { preferLooseListItem } = this
    const parent = this.getParent(block)
    const grandpa = this.getParent(parent)
    const checked = /\[x\]\s/i.test(marker) // use `i` flag to ignore upper case or lower case
    const checkbox = this.createBlock('input')
    const { start, end } = this.cursor

    checkbox.checked = checked
    this.insertBefore(checkbox, block)
    block.text = block.text.substring(marker.length)
    parent.listItemType = 'task'
    parent.isLooseListItem = preferLooseListItem

    let taskListWrapper
    if (this.isOnlyChild(parent)) {
      grandpa.listType = 'task'
    } else if (this.isFirstChild(parent) || this.isLastChild(parent)) {
      taskListWrapper = this.createBlock('ul')
      taskListWrapper.listType = 'task'
      this.isFirstChild(parent) ? this.insertBefore(taskListWrapper, grandpa) : this.insertAfter(taskListWrapper, grandpa)
      this.removeBlock(parent)
      this.appendChild(taskListWrapper, parent)
    } else {
      taskListWrapper = this.createBlock('ul')
      taskListWrapper.listType = 'task'
      const bulletListWrapper = this.createBlock('ul')
      bulletListWrapper.listType = 'bullet'

      let preSibling = this.getPreSibling(parent)
      while (preSibling) {
        this.removeBlock(preSibling)
        if (bulletListWrapper.children.length) {
          const firstChild = bulletListWrapper.children[0]
          this.insertBefore(preSibling, firstChild)
        } else {
          this.appendChild(bulletListWrapper, preSibling)
        }
        preSibling = this.getPreSibling(preSibling)
      }

      this.removeBlock(parent)
      this.appendChild(taskListWrapper, parent)
      this.insertBefore(taskListWrapper, grandpa)
      this.insertBefore(bulletListWrapper, taskListWrapper)
    }

    this.cursor = {
      start: {
        key: start.key,
        offset: Math.max(0, start.offset - marker.length)
      },
      end: {
        key: end.key,
        offset: Math.max(0, end.offset - marker.length)
      }
    }
  }

  // handle task list item checkbox click
  ContentState.prototype.listItemCheckBoxClick = function (checkbox) {
    const { checked, id } = checkbox
    const block = this.getBlock(id)
    block.checked = checked
    this.render()
  }

  ContentState.prototype.checkSameLooseType = function (list, isLooseType) {
    return list.children[0].isLooseListItem === isLooseType
  }

  ContentState.prototype.updateList = function (block, type, marker = '') {
    const { preferLooseListItem } = this
    const parent = this.getParent(block)
    const preSibling = this.getPreSibling(block)
    const nextSibling = this.getNextSibling(block)
    const wrapperTag = type === 'order' ? 'ol' : 'ul' // `bullet` => `ul` and `order` => `ol`
    const newText = block.text.substring(marker.length)
    const { start, end } = this.cursor
    const startOffset = start.offset
    const endOffset = end.offset
    const newBlock = this.createBlockLi(newText, block.type)
    newBlock.listItemType = type
    newBlock.isLooseListItem = preferLooseListItem

    if (
      preSibling && preSibling.listType === type && this.checkSameLooseType(preSibling, preferLooseListItem) &&
      nextSibling && nextSibling.listType === type && this.checkSameLooseType(nextSibling, preferLooseListItem)
    ) {
      this.appendChild(preSibling, newBlock)
      const partChildren = nextSibling.children.splice(0)
      partChildren.forEach(b => this.appendChild(preSibling, b))
      this.removeBlock(nextSibling)
      this.removeBlock(block)
    } else if (preSibling && preSibling.type === wrapperTag && this.checkSameLooseType(preSibling, preferLooseListItem)) {
      this.appendChild(preSibling, newBlock)

      this.removeBlock(block)
    } else if (nextSibling && nextSibling.listType === type && this.checkSameLooseType(nextSibling, preferLooseListItem)) {
      this.insertBefore(newBlock, nextSibling.children[0])

      this.removeBlock(block)
    } else if (parent && parent.listType === type && this.checkSameLooseType(parent, preferLooseListItem)) {
      this.insertBefore(newBlock, block)

      this.removeBlock(block)
    } else {
      block.type = wrapperTag
      block.listType = type // `bullet` or `order`
      block.text = ''
      if (wrapperTag === 'ol') {
        const start = marker.split('.')[0]
        block.start = /^\d+$/.test(start) ? start : 1
      }
      this.appendChild(block, newBlock)
    }

    const key = newBlock.type === 'li' ? newBlock.children[0].key : newBlock.key
    this.cursor = {
      start: {
        key,
        offset: Math.max(0, startOffset - marker.length)
      },
      end: {
        key,
        offset: Math.max(0, endOffset - marker.length)
      }
    }
    return newBlock.children[0]
  }

  ContentState.prototype.updateBlockQuote = function (block) {
    const newText = block.text.substring(1).trim()
    const newPblock = this.createBlock('p', newText)
    block.type = 'blockquote'
    block.text = ''
    this.appendChild(block, newPblock)

    const { start, end } = this.cursor
    const key = newPblock.key
    this.cursor = {
      start: {
        key,
        offset: start.offset - 1
      },
      end: {
        key,
        offset: end.offset - 1
      }
    }
  }

  // thematic break
  ContentState.prototype.updateHr = function (block, marker) {
    block.type = 'hr'
  }

  ContentState.prototype.updateState = function (event) {
    const { floatBox } = this
    const { start, end } = selection.getCursorRange()
    const key = start.key
    const block = this.getBlock(key)
    // bugfix: #67 problem 1
    if (block && block.icon) return event.preventDefault()

    const { start: oldStart, end: oldEnd } = this.cursor

    if (event.type === 'keyup' && (event.key === 'ArrowUp' || event.key === 'ArrowDown') && floatBox.show) {
      return
    }

    if (event.type === 'click' && start.key !== end.key) {
      setTimeout(() => {
        this.updateState(event)
      })
    }

    if (event.type === 'input' && oldStart.key !== oldEnd.key) {
      const startBlock = this.getBlock(oldStart.key)
      const endBlock = this.getBlock(oldEnd.key)
      this.removeBlocks(startBlock, endBlock)
      // there still has little bug, when the oldstart block is `pre`, the input value will be ignored.
      // and act as `backspace`
      if (startBlock.type === 'pre') {
        event.preventDefault()
        const startRemainText = startBlock.type === 'pre'
          ? startBlock.text.substring(0, oldStart.offset - 1)
          : startBlock.text.substring(0, oldStart.offset)

        const endRemainText = endBlock.type === 'pre'
          ? endBlock.text.substring(oldEnd.offset - 1)
          : endBlock.text.substring(oldEnd.offset)

        startBlock.text = startRemainText + endRemainText
        const key = oldStart.key
        const offset = oldStart.offset
        this.cursor = {
          start: { key, offset },
          end: { key, offset }
        }
        return this.render()
      }
    }

    if (start.key !== end.key) {
      if (
        start.key !== oldStart.key ||
        end.key !== oldEnd.key ||
        start.offset !== oldStart.offset ||
        end.offset !== oldEnd.offset
      ) {
        this.cursor = { start, end }
        // this.render()
      }
      return
    }

    const oldKey = lastCursor ? lastCursor.start.key : null
    const paragraph = document.querySelector(`#${key}`)
    let text = getTextContent(paragraph, [ CLASS_OR_ID['AG_MATH_RENDER'] ])
    let needRender = false
    if (event.type === 'click' && block.type === 'figure' && block.functionType === 'table') {
      // first cell in thead
      const cursorBlock = block.children[1].children[0].children[0].children[0]
      const offset = cursorBlock.text.length
      const key = cursorBlock.key
      this.cursor = {
        start: { key, offset },
        end: { key, offset }
      }
      return this.render()
    }
    // remove temp block which generated by operation on code block
    if (block && block.key !== oldKey) {
      const oldBlock = this.getBlock(oldKey)
      if (oldBlock) this.codeBlockUpdate(oldBlock)
      if (oldBlock && oldBlock.temp) {
        if (oldBlock.text || oldBlock.children.length) {
          delete oldBlock.temp
        } else {
          this.removeBlock(oldBlock)
          needRender = true
        }
      }
    }

    if (block && block.type === 'pre') {
      if (block.key !== oldKey) {
        lastCursor = this.cursor = { start, end }
        this.render()
      }
      return
    }

    if (block && block.text !== text) {
      const BRACKET_HASH = {
        '{': '}',
        '[': ']',
        '(': ')',
        '*': '*',
        '_': '_',
        '"': '"',
        "'": "'"
      }
      if (start.key === end.key && start.offset === end.offset && event.type === 'input' && BRACKET_HASH[event.data]) {
        const { offset } = start
        const {
          autoPairBracket, autoPairMarkdownSyntax, autoPairQuote
        } = this
        const inputChar = text.charAt(+offset - 1)
        /* eslint-disable no-useless-escape */
        if (
          (autoPairQuote && /["']{1}/.test(inputChar)) ||
          (autoPairBracket && /[\{\[\(]{1}/.test(inputChar)) ||
          (autoPairMarkdownSyntax && /[*_]{1}/.test(inputChar))
        ) {
          text = text.substring(0, offset) + BRACKET_HASH[inputChar] + text.substring(offset)
        }
        /* eslint-ensable no-useless-escape */
      }
      block.text = text
    }

    if (oldKey !== key || oldStart.offset !== start.offset || oldEnd.offset !== end.offset) {
      needRender = true
    }

    this.cursor = lastCursor = { start, end }

    const checkMarkedUpdate = this.checkNeedRender(block)
    const checkInlineUpdate = this.checkInlineUpdate(block)

    if (checkMarkedUpdate || checkInlineUpdate || needRender) {
      this.render()
    }
  }
}

export default updateCtrl


import { VIEW_MENU_ITEM } from '../config'
import * as actions from '../actions/view'

let viewMenu = {
  label: 'View',
  submenu: [{
    label: 'Toggle Full Screen',
    accelerator: (function () {
      if (process.platform === 'darwin') {
        return 'Ctrl+Command+F'
      } else {
        return 'F11'
      }
    })(),
    click: function (item, focusedWindow) {
      if (focusedWindow) {
        focusedWindow.setFullScreen(!focusedWindow.isFullScreen())
      }
    }
  }, {
    type: 'separator'
  }, {
    label: 'Font...',
    accelerator: 'CmdOrCtrl+.',
    click (item, browserWindow) {
      actions.changeFont(browserWindow)
    }
  }, {
    type: 'separator'
  }, {
    label: 'Source Code Mode',
    accelerator: 'CmdOrCtrl+U',
    type: 'checkbox',
    checked: VIEW_MENU_ITEM['Source Code Mode'],
    click (item, browserWindow) {
      actions.typeMode(browserWindow, item, 'sourceCode')
    }
  }, {
    label: 'Typewriter Mode',
    accelerator: 'Shift+CmdOrCtrl+T',
    type: 'checkbox',
    checked: VIEW_MENU_ITEM['Typewriter Mode'],
    click (item, browserWindow) {
      actions.typeMode(browserWindow, item, 'typewriter')
    }
  }, {
    label: 'Focus Mode',
    accelerator: 'Shift+CmdOrCtrl+F',
    type: 'checkbox',
    checked: VIEW_MENU_ITEM['Focus Mode'],
    click (item, browserWindow) {
      actions.typeMode(browserWindow, item, 'focus')
    }
  }, {
    type: 'separator'
  }]
}

if (process.env.NODE_ENV !== 'production' || process.env.NODE_ENV === 'development') {
  // add devtool when development
  viewMenu.submenu.push({
    label: 'Toggle Developer Tools',
    accelerator: (function () {
      if (process.platform === 'darwin') {
        return 'Alt+Command+I'
      } else {
        return 'Ctrl+Shift+I'
      }
    })(),
    click: function (item, focusedWindow) {
      if (focusedWindow) {
        focusedWindow.webContents.toggleDevTools()
      }
    }
  })
  // add reload when development
  viewMenu.submenu.push({
    label: 'Reload',
    accelerator: 'CmdOrCtrl+R',
    click: function (item, focusedWindow) {
      if (focusedWindow) {
        focusedWindow.reload()
      }
    }
  })
}

if (process.platform === 'darwin') {
  viewMenu.submenu.push({
    type: 'separator'
  }, {
    label: 'Bring All to Front',
    role: 'front'
  })
}

export default viewMenu


const LIST_REG = /ul|ol/

const pasteCtrl = ContentState => {
  // check paste type: `MERGE` or `NEWLINE`
  ContentState.prototype.checkPasteType = function (start, fragment) {
    const fragmentType = fragment.type
    if (fragmentType === 'p') return 'MERGE'
    if (fragmentType === 'blockquote') return 'NEWLINE'
    let parent = this.getParent(start)
    if (parent && parent.type === 'li') parent = this.getParent(parent)
    let startType = start.type
    if (start.type === 'p') {
      startType = parent ? parent.type : startType
    }
    if (LIST_REG.test(fragmentType) && LIST_REG.test(startType)) {
      return 'MERGE'
    } else {
      return startType === fragmentType ? 'MERGE' : 'NEWLINE'
    }
  }

  ContentState.prototype.pasteHandler = function (event) {
    if (this.checkInCodeBlock()) {
      return
    }
    event.preventDefault()
    const text = event.clipboardData.getData('text/plain')
    let html = event.clipboardData.getData('text/html')

    if (!html) {
      html = text.split(/\n+/)
        .filter(t => t)
        .map(t => `<p>${t}</p>`)
        .join('')
    }

    const stateFragments = this.html2State(html)

    if (stateFragments.length <= 0) return
    // step 1: if select content, cut the content, and chop the block text into two part by the cursor.
    const { start, end } = this.cursor
    const startBlock = this.getBlock(start.key)
    const endBlock = this.getBlock(end.key)
    const cacheText = endBlock.text.substring(end.offset)
    startBlock.text = startBlock.text.substring(0, start.offset)
    if (start.key !== end.key) {
      this.cutHandler()
    }
    // step 2: when insert the fragments, check begin a new block, or insert into pre block.
    const firstFragment = stateFragments[0]
    const tailFragments = stateFragments.slice(1)
    const pasteType = this.checkPasteType(startBlock, firstFragment)
    const getLastBlock = blocks => {
      const len = blocks.length
      const lastBlock = blocks[len - 1]

      if (lastBlock.children.length === 0) {
        return lastBlock
      } else {
        return getLastBlock(lastBlock.children)
      }
    }
    const lastBlock = getLastBlock(stateFragments)
    let key = lastBlock.key
    let offset = lastBlock.text.length
    lastBlock.text += cacheText
    switch (pasteType) {
      case 'MERGE':
        if (LIST_REG.test(firstFragment.type)) {
          const children = firstFragment.children
          const firstChildOfFirstFragment = children[0]
          const liChildren = firstChildOfFirstFragment.children
          const startParent = this.getParent(startBlock)
          const liParent = this.getParent(this.getParent(startBlock))
          if (liChildren[0].type === 'p') {
            startBlock.text += liChildren[0].text
            const tail = liChildren.slice(1)
            if (tail.length) {
              tail.forEach(t => {
                this.appendChild(startParent, t)
              })
            }
            const firstFragmentTail = children.slice(1)
            if (firstFragmentTail.length) {
              firstFragmentTail.forEach(t => {
                this.appendChild(liParent, t)
              })
            }
          } else {
            children.forEach(c => {
              this.appendChild(liParent, c)
            })
          }
          let target = liParent
          tailFragments.forEach(block => {
            this.insertAfter(block, target)
            target = block
          })
        } else {
          startBlock.text += firstFragment.text
          let target = startBlock
          tailFragments.forEach(block => {
            this.insertAfter(block, target)
            target = block
          })
        }
        break

      case 'NEWLINE':
        let target = startBlock
        stateFragments.forEach(block => {
          this.insertAfter(block, target)
          target = block
        })
        if (startBlock.text.length === 0) this.removeBlock(startBlock)
        break
      default:
        throw new Error('unknown paste type')
    }
    // step 3: set cursor and render
    const cursorBlock = this.getBlock(key)
    if (!cursorBlock) {
      key = startBlock.key
      offset = startBlock.text.length - cacheText.length
    }
    this.cursor = {
      start: {
        key, offset
      },
      end: {
        key, offset
      }
    }
    this.render()
  }
}

export default pasteCtrl


'use strict'

// Set BABEL_ENV to use proper env config
process.env.BABEL_ENV = 'test'

// Enable use of ES6+ on required files
require('babel-register')({
  ignore: /node_modules/
})

// Attach Chai APIs to global scope
const { expect, should, assert } = require('chai')
global.expect = expect
global.should = should
global.assert = assert

// Require all JS files in `./specs` for Mocha to consume
require('require-dir')('./specs')


if (start.key === end.key && start.offset === end.offset && event.type === 'input' && BRACKET_HASH[event.data]) {

if (start.key === end.key && start.offset === end.offset && event.type === 'input') {

import fileIcons from '../fileIcons'
import {
  CLASS_OR_ID
} from '../config'

import './index.css'

const FLOAT_BOX_HEIGHT = 180
const ITEM_HEIGHT = 28

class FloatBox {
  constructor (eventCenter) {
    this.list = []
    this.type = ''
    this.index = 0
    this.position = null
    this.eventCenter = eventCenter
    this.show = false
    this.box = null
    this.cb = null
    this.initBox()
  }

  initBox () {
    let box = document.querySelector(`#${CLASS_OR_ID['AG_FLOAT_BOX_ID']}`)
    const clickHandler = event => {
      const target = event.target
      const key = +target.getAttribute('key')
      const { cb, list } = this

      if (cb && typeof key === 'number' && !Number.isNaN(key)) {
        this.cb(list[key])
        this.hideIfNeeded([this.type])
      }
    }

    if (!box) {
      box = document.createElement('ul')
      box.id = CLASS_OR_ID['AG_FLOAT_BOX_ID']
      box.classList.add(CLASS_OR_ID['AG_FLOAT_BOX'])
      document.body.appendChild(box)
      this.eventCenter.attachDOMEvent(box, 'click', clickHandler)
    }
    this.box = box
  }

  setOptions (list, index) {
    const fragment = document.createDocumentFragment()
    this.list = list
    if (index !== undefined) {
      this.index = index
    }

    list.forEach((l, i) => {
      const li = document.createElement('li')
      li.classList.add(CLASS_OR_ID['AG_FLOAT_ITEM'])
      if (i === this.index) li.classList.add(CLASS_OR_ID['AG_FLOAT_ITEM_ACTIVE'])
      // for emoji
      if (l.emoji) {
        const icon = document.createElement('span')
        icon.classList.add(CLASS_OR_ID['AG_FLOAT_ITEM_ICON'])
        icon.setAttribute('key', i)
        icon.textContent = l.emoji
        li.appendChild(icon)
      } else if (l.mode) { // for language
        let className
        if (l.mode.ext && Array.isArray(l.mode.ext)) {
          for (const ext of l.mode.ext) {
            className = fileIcons.getClassWithColor(`fackname.${ext}`)
            if (className) break
          }
        } else if (l.mode.name) {
          className = fileIcons.getClassWithColor(l.mode.name)
        }

        // Because `markdown mode in Codemirror` don't have extensions.
        // if still can not get the className, add a common className 'atom-icon light-cyan'
        if (!className) {
          className = l.text === 'markdown' ? fileIcons.getClassWithColor('fackname.md') : 'atom-icon light-cyan'
        }

        const icon = document.createElement('span')
        icon.classList.add(...className.split(/\s/))
        icon.classList.add(CLASS_OR_ID['AG_FLOAT_ITEM_ICON'])
        icon.setAttribute('key', i)
        li.appendChild(icon)
      } else if (l.iconClass) {
        const { iconClass } = l
        const icon = document.createElement('span')
        icon.innerHTML = `
          <svg class="icon" aria-hidden="true">
            <use xlink:href="#${iconClass}"></use>
          </svg>
        `
        icon.classList.add(CLASS_OR_ID['AG_FLOAT_ITEM_ICON'])
        icon.setAttribute('key', i)
        li.appendChild(icon)
      }
      const text = document.createElement('span')

      ;[text, li].forEach(ele => {
        ele.setAttribute('key', i)
      })

      text.textContent = l.text
      li.appendChild(text)

      fragment.appendChild(li)
    })
    this.empty()
    this.box.appendChild(fragment)
    this.box.scrollTop = ITEM_HEIGHT * this.index
  }

  empty () {
    Array.from(this.box.childNodes).forEach(c => this.box.removeChild(c))
  }

  showIfNeeded (position, type, cb) {
    if (cb) this.cb = cb
    if (!this.show || this.type !== type || position.left !== this.position.left || position.top !== this.position.top) {
      let { left, top } = this.position = position
      this.type = type
      const viewHeight = document.documentElement.offsetHeight
      if (viewHeight - top <= FLOAT_BOX_HEIGHT + 25) {
        top = top - (FLOAT_BOX_HEIGHT + 5) // left 5px between floatbox and input element
      } else {
        top = top + 25
      }
      Object.assign(this.box.style, { left: `${left}px`, top: `${top}px` })

      this.box.classList.add(CLASS_OR_ID['AG_SHOW_FLOAT_BOX'])
    }
    this.show = true
  }

  hideIfNeeded (typeArray) {
    if (!typeArray.includes(this.type)) return
    this.empty()
    this.cb = null
    this.list = []
    this.index = 0
    if (this.show) {
      this.box.classList.remove(CLASS_OR_ID['AG_SHOW_FLOAT_BOX'])
      this.box.removeAttribute('style')
    }
    this.show = false
  }

  destroy () {
    this.box.parentNode.removeChild(this.box)
  }
}

export default FloatBox


export const EXTENSIONS = [
  'markdown',
  'mdown',
  'mkdn',
  'md',
  'mkd',
  'mdwn',
  'mdtxt',
  'mdtext',
  'text',
  'txt'
]

export const IMAGE_EXTENSIONS = [
  'jpeg',
  'jpg',
  'png',
  'gif',
  'svg',
  'webp'
]

export const BLACK_LIST = [
  '$RECYCLE.BIN'
]

export const EXTENSION_HASN = {
  styledHtml: '.html',
  html: '.html',
  pdf: '.pdf'
}

export const VIEW_MENU_ITEM = {
  'Source Code Mode': false,
  'Typewriter Mode': false,
  'Focus Mode': false
}


const inputChar = text.charAt(+offset - 1)

const { autoPairBracket, autoPairMarkdownSyntax, autoPairQuote } = this


const languages = [{
  'name': 'objectivec',
  'mode': 'clike',
  'mime': 'text/x-objectivec'
}, {
  'name': 'swift',
  'mode': 'swift',
  'mime': 'text/x-swift'
}, {
  'name': 'c_cpp',
  'mode': 'clike',
  'mime': 'text/x-csrc'
}, {
  'name': 'c',
  'mode': 'clike',
  'mime': 'text/x-csrc'
}, {
  'name': 'c++',
  'mode': 'clike',
  'mime': 'text/x-c++src'
}, {
  'name': 'cmake',
  'mode': 'cmake',
  'mime': 'text/x-cmake'
}, {
  'name': 'lisp',
  'mode': 'commonlisp',
  'mime': 'text/x-common-lisp'
}, {
  'name': 'pascal',
  'mode': 'pascal',
  'mime': 'text/x-pascal'
}, {
  'name': 'eiffel',
  'mode': 'eiffel',
  'mime': 'text/x-eiffel'
}, {
  'name': 'yaml',
  'mode': 'yaml',
  'mime': 'text/x-yaml'
}, {
  'name': 'xml',
  'mode': 'xml',
  'mime': 'application/xml'
}, {
  'name': 'django',
  'mode': 'django',
  'mime': 'text/x-django'
}, {
  'name': 'clojure',
  'mode': 'clojure',
  'mime': 'text/x-clojure'
}, {
  'name': 'crystal',
  'mode': 'crystal',
  'mime': 'text/x-crystal'
}, {
  'name': 'ruby',
  'mode': 'ruby',
  'mime': 'text/x-ruby'
}, {
  'name': 'python',
  'mode': 'python',
  'mime': 'text/x-python'
}, {
  'name': 'sh',
  'mode': 'shell',
  'mime': 'text/x-sh'
}, { /* alias */
  'name': 'shell',
  'mode': 'shell',
  'mime': 'text/x-sh'
}, {
  'name': 'less',
  'mode': 'css',
  'mime': 'text/css'
}, {
  'name': 'php',
  'mode': 'php',
  'mime': 'application/x-httpd-php'
}, {
  'name': 'json',
  'mode': 'javascript',
  'mime': 'application/json'
}, {
  'name': 'smarty',
  'mode': 'smarty',
  'mime': 'text/x-smarty'
}, {
  'name': 'cobol',
  'mode': 'cobol',
  'mime': 'text/x-cobol'
}, {
  'name': 'go',
  'mode': 'go',
  'mime': 'text/x-go'
}, { /* alias */
  'name': 'golang',
  'mode': 'go',
  'mime': 'text/x-go'
}, {
  'name': 'makefile',
  'mode': 'shell', /* makefile syntax is not supported by CodeMirror */
  'mime': 'text/x-sh'
}, {
  'name': 'ocaml',
  'mode': 'mllike',
  'mime': 'text/x-ocaml'
}, {
  'name': 'textile',
  'mode': 'textile',
  'mime': 'text/x-textile'
}, {
  'name': 'd',
  'mode': 'd',
  'mime': 'text/x-d'
}, {
  'name': 'jade',
  'mode': 'pug',
  'mime': 'text/x-pug'
}, {
  'name': 'lua',
  'mode': 'lua',
  'mime': 'text/x-lua'
}, {
  'name': 'coffee',
  'mode': 'coffeescript',
  'mime': 'text/x-coffeescript'
}, {
  'name': 'html',
  'mode': 'htmlmixed',
  'mime': 'text/html'
}, {
  'name': 'pgsql',
  'mode': 'sql',
  'mime': 'text/x-sql'
}, {
  'name': 'haskell',
  'mode': 'haskell',
  'mime': 'text/x-haskell'
}, {
  'name': 'jsp',
  'mode': 'htmlembedded',
  'mime': 'application/x-jsp'
}, {
  'name': 'tcl',
  'mode': 'tcl',
  'mime': 'text/x-tcl'
}, {
  'name': 'ini',
  'mode': 'properties',
  'mime': 'text/x-properties'
}, {
  'name': 'jsoniq',
  'mode': 'javascript',
  'mime': 'application/json'
}, {
  'name': 'vhdl',
  'mode': 'vhdl',
  'mime': 'text/x-vhdl'
}, {
  'name': 'verilog',
  'mode': 'verilog',
  'mime': 'text/x-systemverilog'
}, {
  'name': 'csharp',
  'mode': 'clike',
  'mime': 'text/x-csharp'
}, {
  'name': 'rust',
  'mode': 'rust',
  'mime': 'text/x-rustsrc'
}, {
  'name': 'livescript',
  'mode': 'livescript',
  'mime': 'text/x-livescript'
}, {
  'name': 'jsx',
  'mode': 'jsx',
  'mime': 'text/jsx'
}, {
  'name': 'protobuf',
  'mode': 'protobuf',
  'mime': 'text/x-protobuf'
}, {
  'name': 'markdown',
  'mode': 'gfm',
  'mime': 'text/x-gfm'
}, {
  'name': 'rst',
  'mode': 'rst',
  'mime': 'text/x-rst'
}, {
  'name': 'LaTeX',
  'mode': 'stex',
  'mime': 'text/x-latex'
}, {
  'name': 'java',
  'mode': 'clike',
  'mime': 'text/x-java'
}, {
  'name': 'kotlin',
  'mode': 'clike',
  'mime': 'text/x-kotlin'
}, {
  'name': 'javascript',
  'mode': 'javascript',
  'mime': 'text/javascript'
}, {
  'name': 'erlang',
  'mode': 'erlang',
  'mime': 'text/x-erlang'
}, {
  'name': 'scheme',
  'mode': 'scheme',
  'mime': 'text/x-scheme'
}, {
  'name': 'sass',
  'mode': 'sass',
  'mime': 'text/x-sass'
}, {
  'name': 'groovy',
  'mode': 'groovy',
  'mime': 'text/x-groovy'
}, {
  'name': 'julia',
  'mode': 'julia',
  'mime': 'text/x-julia'
}, {
  'name': 'haml',
  'mode': 'haml',
  'mime': 'text/x-haml'
}, {
  'name': 'powershell',
  'mode': 'powershell',
  'mime': 'application/x-powershell'
}, {
  'name': 'typescript',
  'mode': 'javascript',
  'mime': 'application/typescript'
}, {
  'name': 'dart',
  'mode': 'dart',
  'mime': 'application/dart'
}, {
  'name': 'xquery',
  'mode': 'xquery',
  'mime': 'application/xquery'
}, {
  'name': 'elm',
  'mode': 'elm',
  'mime': 'text/x-elm'
}, {
  'name': 'plsql',
  'mode': 'sql',
  'mime': 'text/x-plsql'
}, {
  'name': 'forth',
  'mode': 'forth',
  'mime': 'text/x-forth'
}, {
  'name': 'scala',
  'mode': 'clike',
  'mime': 'text/x-scala'
}, {
  'name': 'perl',
  'mode': 'perl',
  'mime': 'text/x-perl'
}, {
  'name': 'haxe',
  'mode': 'haxe',
  'mime': 'text/x-haxe'
}, {
  'name': 'rhtml',
  'mode': 'htmlembedded',
  'mime': 'application/x-erb'
}, {
  'name': 'scss',
  'mode': 'css',
  'mime': 'text/x-scss'
}, {
  'name': 'sql',
  'mode': 'sql',
  'mime': 'text/x-sql'
}, {
  'name': 'css',
  'mode': 'css',
  'mime': 'text/css'
}, {
  'name': 'tex',
  'mode': 'stex',
  'mime': 'text/x-stex'
}, {
  'name': 'r',
  'mode': 'r',
  'mime': 'text/x-rsrc'
}, {
  'name': 'diff',
  'mode': 'diff',
  'mime': 'text/x-diff'
}, {
  'name': 'twig',
  'mode': 'twig',
  'mime': 'text/x-twig'
}, {
  'name': 'matlab',
  'mode': 'octave',
  'mime': 'text/x-octave'
}, {
  'name': 'soy_template',
  'mode': 'soy',
  'mime': 'text/x-soy'
}, {
  'name': 'dockerfile',
  'mode': 'dockerfile',
  'mime': 'text/x-dockerfile'
}, {
  'name': 'toml',
  'mode': 'toml',
  'mime': 'text/x-toml'
}, {
  'name': 'pgp',
  'mode': 'asciiarmor',
  'mime': 'application/pgp'
}, {
  'name': 'Nginx',
  'mode': 'nginx',
  'mime': 'text/x-nginx-conf'
}]

export default languages


import { app } from 'electron'
import * as actions from '../actions/file'
import { userSetting } from '../actions/marktext'
import userPreference from '../preference'

export default function (recentlyUsedFiles) {
  const { autoSave } = userPreference.getAll()
  const notOsx = process.platform !== 'darwin'
  let fileMenu = {
    label: 'File',
    submenu: [{
      label: 'New File',
      accelerator: 'CmdOrCtrl+N',
      click (menuItem, browserWindow) {
        actions.newFile()
      }
    }, {
      label: 'Open...',
      accelerator: 'CmdOrCtrl+O',
      click (menuItem, browserWindow) {
        actions.open(browserWindow)
      }
    }]
  }

  if (notOsx) {
    let recentlyUsedMenu = {
      label: 'Open Recent',
      submenu: []
    }

    for (const item of recentlyUsedFiles) {
      recentlyUsedMenu.submenu.push({
        label: item,
        click (menuItem, browserWindow) {
          actions.openDocument(menuItem.label)
        }
      })
    }

    recentlyUsedMenu.submenu.push({
      type: 'separator',
      visible: recentlyUsedFiles.length > 0
    }, {
      label: 'Clear Recently Used',
      enabled: recentlyUsedFiles.length > 0,
      click (menuItem, browserWindow) {
        actions.clearRecentlyUsed()
      }
    })
    fileMenu.submenu.push(recentlyUsedMenu)
  } else {
    fileMenu.submenu.push({
      role: 'recentdocuments',
      submenu: [
        {
          role: 'clearrecentdocuments'
        }
      ]
    })
  }

  fileMenu.submenu.push({
    type: 'separator'
  }, {
    label: 'Save',
    accelerator: 'CmdOrCtrl+S',
    click (menuItem, browserWindow) {
      actions.save(browserWindow)
    }
  }, {
    label: 'Save As...',
    accelerator: 'Shift+CmdOrCtrl+S',
    click (menuItem, browserWindow) {
      actions.saveAs(browserWindow)
    }
  }, {
    label: 'Auto Save',
    type: 'checkbox',
    checked: autoSave,
    click (menuItem, browserWindow) {
      actions.autoSave(menuItem, browserWindow)
    }
  }, {
    type: 'separator'
  }, {
    label: 'Move To...',
    click (menuItem, browserWindow) {
      actions.moveTo(browserWindow)
    }
  }, {
    label: 'Rename...',
    click (menuItem, browserWindow) {
      actions.rename(browserWindow)
    }
  }, {
    type: 'separator'
  }, {
    label: 'Export Styled HTML',
    click (menuItem, browserWindow) {
      actions.exportFile(browserWindow, 'styledHtml')
    }
  }, {
    label: 'Export HTML',
    click (menuItem, browserWindow) {
      actions.exportFile(browserWindow, 'html')
    }
  }, {
    label: 'Export PDF',
    click (menuItem, browserWindow) {
      actions.exportFile(browserWindow, 'pdf')
    }
  }, {
    type: 'separator'
  }, {
    label: 'Print',
    accelerator: 'CmdOrCtrl+P',
    click (menuItem, browserWindow) {
      actions.print(browserWindow)
    }
  }, {
    type: 'separator',
    visible: notOsx
  }, {
    label: 'Preferences',
    accelerator: 'Ctrl+,',
    visible: notOsx,
    click (menuItem, browserWindow) {
      userSetting(menuItem, browserWindow)
    }
  }, {
    type: 'separator',
    visible: notOsx
  }, {
    label: 'Quit',
    accelerator: 'Ctrl+Q',
    visible: notOsx,
    click: app.quit
  })
  return fileMenu
}


(autoPairQuote && /["']{1}/.test(inputChar)) ||

(autoPairBracket && /[\{\[\(]{1}/.test(inputChar)) ||

const preInputChar = text.charAt(+offset - 2)

const postInputChar = text.charAt(+offset)


import utils from '../utils'

describe('Launch', function () {
  beforeEach(utils.beforeEach)
  afterEach(utils.afterEach)

  it('shows the proper application title', function () {
    return this.app.client.getTitle()
      .then(title => {
        expect(title).to.equal('aganippe')
      })
  })
})


/**
 * check edit language
 */
export const checkEditLanguage = (paragraph, selectionState) => {
  const text = paragraph.textContent
  const { start } = selectionState
  const token = text.match(/(^`{3,})([^`]+)/)

  if (token) {
    const len = token[1].length
    const lang = token[2].trim()
    if (start < len) return false
    if (!lang) return false
    return lang
  } else {
    return false
  }
}


text = text.substring(0, offset) + BRACKET_HASH[inputChar] + text.substring(offset)

? text.substring(0, offset) + BRACKET_HASH[inputChar] + text.substring(offset)


import { shell } from 'electron'
import * as actions from '../actions/help'
import { checkUpdates } from '../actions/marktext'

const notOsx = process.platform !== 'darwin'

export default {
  label: 'Help',
  role: 'help',
  submenu: [{
    label: 'Learn More',
    click: function () {
      shell.openExternal('https://github.com/marktext/marktext')
    }
  }, {
    label: 'Report Issue',
    click: function () {
      shell.openExternal('https://github.com/marktext/marktext/issues')
    }
  }, {
    label: 'Source Code on GitHub',
    click: function () {
      shell.openExternal('https://github.com/marktext/marktext')
    }
  }, {
    label: 'Changelog',
    click: function () {
      shell.openExternal('https://github.com/marktext/marktext/blob/master/.github/CHANGELOG.md')
    }
  }, {
    label: 'Markdown syntax',
    click: function () {
      shell.openExternal('https://spec.commonmark.org/0.28/')
    }
  }, {
    type: 'separator'
  }, {
    label: 'Follow @Jocs on Github',
    click: function () {
      shell.openExternal('https://github.com/Jocs')
    }
  }, {
    type: 'separator',
    visible: notOsx
  }, {
    label: 'Check for updates...',
    visible: notOsx,
    click (menuItem, browserWindow) {
      checkUpdates(menuItem, browserWindow)
    }
  }, {
    type: 'separator',
    visible: notOsx
  }, {
    label: 'About Mark Text',
    visible: notOsx,
    click (menuItem, browserWindow) {
      actions.showAboutDialog(browserWindow)
    }
  }]
}


import Vue from 'vue'
import Vuex from 'vuex'

import editorStore from './editor'
import aidouStore from './aidou'
import autoUpdates from './autoUpdates'

Vue.use(Vuex)

const storeArray = [
  editorStore,
  aidouStore,
  autoUpdates
]

const { actions, mutations, state } = storeArray.reduce((acc, s) => {
  const {actions, mutations, state} = s
  return {
    actions: Object.assign({}, acc.actions, actions),
    mutations: Object.assign({}, acc.mutations, mutations),
    state: Object.assign({}, acc.state, state)
  }
}, {
  actions: {},
  mutations: {},
  state: {}
})

const store = new Vuex.Store({
  actions,
  mutations,
  state,
  getters: {}
})

export default store


if (/\s/.test(event.data) && preInputChar === '*' && postInputChar === '*') {

text = text.substring(0, offset) + text.substring(offset + 1)


/**
 * Set `__static` path to static files in production
 * https://simulatedgreg.gitbooks.io/electron-vue/content/en/using-static-assets.html
 */

if (process.env.NODE_ENV !== 'development') {
  global.__static = require('path').join(__dirname, '/static').replace(/\\/g, '\\\\')
}


import cheerio from 'cheerio'
import selection from '../selection'
import { CLASS_OR_ID, blockContainerElementNames } from '../config'

const copyCutCtrl = ContentState => {
  ContentState.prototype.cutHandler = function () {
    const { start, end } = this.cursor
    const startBlock = this.getBlock(start.key)
    const endBlock = this.getBlock(end.key)
    startBlock.text = startBlock.text.substring(0, start.offset) + endBlock.text.substring(end.offset)
    if (start.key !== end.key) {
      this.removeBlocks(startBlock, endBlock)
    }
    this.cursor = {
      start,
      end: start
    }
    this.render()
  }

  ContentState.prototype.checkInCodeBlock = function () {
    const { start, end } = selection.getCursorRange()
    const block = this.getBlock(start.key)
    if (start.key === end.key && block.type === 'pre') {
      return true
    }
    return false
  }

  ContentState.prototype.copyCutHandler = function (event) {
    if (this.checkInCodeBlock()) {
      return
    }
    event.preventDefault()
    const html = selection.getSelectionHtml()
    // const xx = event.clipboardData.getData('text/plain')
    const $ = cheerio.load(html)
    const children = $('body').contents()
    let text = ''

    children.each((i, child) => {
      const tagName = child.tagName
      const content = $(child).text()
      if (content) {
        text += blockContainerElementNames.indexOf(tagName) > -1 && text ? `\n${content}` : `${content}`
      }
    })

    $(
      `.${CLASS_OR_ID['AG_REMOVE']}, .${CLASS_OR_ID['AG_TOOL_BAR']},
      .${CLASS_OR_ID['AG_MATH_RENDER']}, .${CLASS_OR_ID['AG_HTML_PREVIEW']},
      .${CLASS_OR_ID['AG_COPY_REMOVE']}`
    ).remove()
    $(`.${CLASS_OR_ID['AG_EMOJI_MARKER']}`).text(':')
    $(`.${CLASS_OR_ID['AG_NOTEXT_LINK']}`).empty()
    $(`[data-role=hr]`).replaceWith('
')

    const aLink = $(`.${CLASS_OR_ID['AG_A_LINK']}`)
    if (aLink.length > 0) {
      aLink.each((i, a) => {
        const anchor = $(a)
        const html = anchor.html()
        anchor.replaceWith(`<span>${html}</span>`)
      })
    }

    const anchors = $(`a[data-href]`)
    if (anchors.length > 0) {
      anchors.each((i, a) => {
        const anchor = $(a)
        const href = anchor.attr('data-href')
        anchor.removeAttr('data-href')
        anchor.attr('href', href)
      })
    }

    const codefense = $(`pre.${CLASS_OR_ID['AG_CODE_BLOCK']}`)
    if (codefense.length > 0) {
      codefense.each((i, cf) => {
        const ele = $(cf)
        const id = ele.attr('id')
        const language = ele.attr('data-lang')
        const cm = this.codeBlocks.get(id)
        const codeText = cm.getValue()
        ele.empty()
        ele.html(`<code class="language-${language}" lang="${language}">${codeText}</code>`)
      })
    }

    const htmlBlock = $(`figure[data-role='HTML']`)
    if (htmlBlock.length > 0) {
      htmlBlock.each((i, hb) => {
        const ele = $(hb)
        const id = ele.attr('id')
        const { text } = this.getBlock(id)
        const pre = $('

')
        pre.text(text)
        ele.replaceWith(pre)
      })
    }

    event.clipboardData.setData('text/html', $('body').html())
    event.clipboardData.setData('text/plain', text)
  }
}

export default copyCutCtrl


import fileIcons from '../fileIcons'
import {
CLASS_OR_ID
} from '../config'
import './index.css'

keep replace replace replace keep keep keep