diff --git a/.babelrc b/.babelrc
new file mode 100644
index 0000000..cbd3ac6
--- /dev/null
+++ b/.babelrc
@@ -0,0 +1,8 @@
+{
+  "presets": [
+    "es2015"
+  ],
+  "plugins": [
+    "transform-runtime"
+  ]
+}
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..121531a
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1 @@
+*.min.js
diff --git a/.eslintrc b/.eslintrc
index 53a6dcb..bd14731 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -16,7 +16,6 @@
         ],
         "array-callback-return": "error",
         "arrow-body-style": "error",
-        "arrow-parens": "error",
         "arrow-spacing": "error",
         "block-scoped-var": "off",
         "block-spacing": "error",
@@ -123,7 +122,7 @@
         "no-extend-native": "error",
         "no-extra-bind": "error",
         "no-extra-label": "error",
-        "no-extra-parens": "error",
+        "no-extra-parens": "warn",
         "no-floating-decimal": "error",
         "no-global-assign": "error",
         "no-implicit-coercion": "error",
@@ -195,7 +194,7 @@
         "no-unneeded-ternary": "error",
         "no-unsafe-negation": "error",
         "no-unused-expressions": "error",
-        "no-use-before-define": "error",
+        "no-use-before-define": "warn",
         "no-useless-call": "error",
         "no-useless-computed-key": "error",
         "no-useless-concat": "error",
diff --git a/.gitignore b/.gitignore
index f48b3c6..ab83c14 100644
--- a/.gitignore
+++ b/.gitignore
@@ -18,7 +18,6 @@ backups/
 
 # ignore config files
 config.json
-public/js/config.js
 .sequelizerc
 
 # ignore webpack build
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..ed8ab42
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,13 @@
+language: node_js
+node_js:
+  - 6
+  - 7
+  - stable
+env:
+  - CXX=g++-4.8
+addons:
+  apt:
+    sources:
+      - ubuntu-toolchain-r-test
+    packages:
+      - g++-4.8
diff --git a/README.md b/README.md
index 2e7b919..2afeba2 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,9 @@
 HackMD
 ===
 
-[![Join the chat at https://gitter.im/hackmdio/hackmd](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/hackmdio/hackmd?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+[![Join the chat at https://gitter.im/hackmdio/hackmd][gitter-image]][gitter-url]
+[![build status][travis-image]][travis-url]
+
 
 HackMD lets you create realtime collaborative markdown notes on all platforms.  
 Inspired by Hackpad, with more focus on speed and flexibility.  
@@ -48,9 +50,9 @@ Browsers Requirement
 Prerequisite
 ---
 
-- Node.js 4.x or up (test up to 6.7.0)
+- Node.js 6.x or up (test up to 7.5.0)
 - Database (PostgreSQL, MySQL, MariaDB, SQLite, MSSQL) use charset `utf8`
-- npm
+- npm (and its dependencies, especially [uWebSockets](https://github.com/uWebSockets/uWebSockets#nodejs-developers), [node-gyp](https://github.com/nodejs/node-gyp#installation))
 
 Get started
 ---
@@ -59,7 +61,7 @@ Get started
 2. Enter the directory and type `bin/setup`, which will install npm dependencies and create configs. The setup script is written in Bash, you would need bash as a prerequisite.
 3. Setup the configs, see more below
 4. Setup environment variables which will overwrite the configs
-5. Build front-end bundle by `npm run build:prod` (use `npm run build:dev` if you are in development)
+5. Build front-end bundle by `npm run build` (use `npm run dev` if you are in development)
 6. Run the server as you like (node, forever, pm2)
 
 Upgrade guide
@@ -70,7 +72,7 @@ If you are upgrading HackMD from an older version, follow these steps:
 1. Fully stop your old server first (important)
 2. `git pull` or do whatever that updates the files
 3. `npm install` to update dependencies
-4. Build front-end bundle by `npm run build:prod` (use `npm run build:dev` if you are in development)
+4. Build front-end bundle by `npm run build` (use `npm run dev` if you are in development)
 5. Modify the file named `.sequelizerc`, change the value of the variable `url` with your db connection string
    For example: `postgres://username:password@localhost:5432/hackmd`
 6. Run `node_modules/.bin/sequelize db:migrate`, this step will migrate your db to the latest schema
@@ -97,19 +99,9 @@ Configuration files
 There are some configs you need to change in the files below
 
 ```
-./config.json			--- for server settings
-./public/js/config.js	--- for client settings
+./config.json      ----application settings
 ```
 
-Client settings `config.js`
----
-
-| variables | example values | description |
-| --------- | ------ | ----------- |
-| debug | `true` or `false` | set debug mode, show more logs |
-| domain | `localhost` | domain name |
-| urlpath | `hackmd` | sub url path, like: `www.example.com/<urlpath>` |
-
 Environment variables (will overwrite other server configs)
 ---
 
@@ -126,6 +118,7 @@ Environment variables (will overwrite other server configs)
 | HMD_USECDN | `true` or `false` | set to use CDN resources or not (default is `true`) |
 | HMD_ALLOW_ANONYMOUS | `true` or `false` | set to allow anonymous usage (default is `true`) |
 | HMD_ALLOW_FREEURL | `true` or `false` | set to allow new note by accessing not exist note url |
+| HMD_DEFAULT_PERMISSION | `freely`, `editable`, `limited`, `locked` or `private` | set notes default permission (only applied on signed users) |
 | HMD_DB_URL | `mysql://localhost:3306/database` | set the db url |
 | HMD_FACEBOOK_CLIENTID | no example | Facebook API client id |
 | HMD_FACEBOOK_CLIENTSECRET | no example | Facebook API client secret |
@@ -140,15 +133,15 @@ Environment variables (will overwrite other server configs)
 | HMD_DROPBOX_CLIENTSECRET | no example | Dropbox API client secret |
 | HMD_GOOGLE_CLIENTID | no example | Google API client id |
 | HMD_GOOGLE_CLIENTSECRET | no example | Google API client secret |
-| HMD_LDAP_URL | ldap://example.com | url of LDAP server |
+| HMD_LDAP_URL | `ldap://example.com` | url of LDAP server |
 | HMD_LDAP_BINDDN | no example | bindDn for LDAP access |
 | HMD_LDAP_BINDCREDENTIALS | no example | bindCredentials for LDAP access |
-| HMD_LDAP_TOKENSECRET | supersecretkey | secret used for generating access/refresh tokens |
-| HMD_LDAP_SEARCHBASE | o=users,dc=example,dc=com | LDAP directory to begin search from |
-| HMD_LDAP_SEARCHFILTER | (uid={{username}}) | LDAP filter to search with |
+| HMD_LDAP_TOKENSECRET | `supersecretkey` | secret used for generating access/refresh tokens |
+| HMD_LDAP_SEARCHBASE | `o=users,dc=example,dc=com` | LDAP directory to begin search from |
+| HMD_LDAP_SEARCHFILTER | `(uid={{username}})` | LDAP filter to search with |
 | HMD_LDAP_SEARCHATTRIBUTES | no example | LDAP attributes to search with |
-| HMD_LDAP_TLS_CA | no example | Root CA for LDAP TLS in PEM format |
-| HMD_LDAP_PROVIDERNAME | My institution | Optional name to be displayed at login form indicating the LDAP provider | 
+| HMD_LDAP_TLS_CA | `server-cert.pem, root.pem` | Root CA for LDAP TLS in PEM format (use comma to separate) |
+| HMD_LDAP_PROVIDERNAME | `My institution` | Optional name to be displayed at login form indicating the LDAP provider | 
 | HMD_IMGUR_CLIENTID | no example | Imgur API client id |
 | HMD_EMAIL | `true` or `false` | set to allow email signin |
 | HMD_ALLOW_EMAIL_REGISTER | `true` or `false` | set to allow email register (only applied when email is set, default is `true`) |
@@ -158,7 +151,7 @@ Environment variables (will overwrite other server configs)
 | HMD_S3_REGION | `ap-northeast-1` | AWS S3 region |
 | HMD_S3_BUCKET | no example | AWS S3 bucket name |
 
-Server settings `config.json`
+Application settings `config.json`
 ---
 
 | variables | example values | description |
@@ -174,6 +167,7 @@ Server settings `config.json`
 | usecdn | `true` or `false` | set to use CDN resources or not (default is `true`) |
 | allowanonymous | `true` or `false` | set to allow anonymous usage (default is `true`) |
 | allowfreeurl | `true` or `false` | set to allow new note by accessing not exist note url |
+| defaultpermission | `freely`, `editable`, `limited`, `locked` or `private` | set notes default permission (only applied on signed users) |
 | dburl | `mysql://localhost:3306/database` | set the db url, if set this variable then below db config won't be applied |
 | db | `{ "dialect": "sqlite", "storage": "./db.hackmd.sqlite" }` | set the db configs, [see more here](http://sequelize.readthedocs.org/en/latest/api/sequelize/) |
 | sslkeypath | `./cert/client.key` | ssl key path (only need when you set usessl) |
@@ -207,7 +201,7 @@ Third-party integration api key settings
 | ------- | --------- | ----------- |
 | facebook, twitter, github, gitlab, dropbox, google, ldap | environment variables or `config.json` | for signin |
 | imgur | environment variables or `config.json` | for image upload |
-| google drive, dropbox | `public/js/config.js` | for export and import |
+| google drive(`google/apiKey`, `google/clientID`), dropbox(`dropbox/appKey`) | `config.json` | for export and import |
 
 Third-party integration oauth callback urls
 ---
@@ -230,3 +224,7 @@ Additionally, now can show other clients' selections.
 See more at [http://operational-transformation.github.io/](http://operational-transformation.github.io/)
 
 **License under MIT.**
+[gitter-image]: https://badges.gitter.im/Join%20Chat.svg
+[gitter-url]: https://gitter.im/hackmdio/hackmd?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge
+[travis-image]: https://travis-ci.org/hackmdio/hackmd.svg?branch=master
+[travis-url]: https://travis-ci.org/hackmdio/hackmd
diff --git a/app.js b/app.js
index ba0b67d..67a6254 100644
--- a/app.js
+++ b/app.js
@@ -26,7 +26,6 @@ var validator = require('validator');
 var config = require("./lib/config.js");
 var logger = require("./lib/logger.js");
 var auth = require("./lib/auth.js");
-var history = require("./lib/history.js");
 var response = require("./lib/response.js");
 var models = require("./lib/models");
 
@@ -443,6 +442,7 @@ app.get('/logout', function (req, res) {
     req.logout();
     res.redirect(config.serverurl + '/');
 });
+var history = require("./lib/history.js");
 //get history
 app.get('/history', history.historyGet);
 //post history
@@ -502,7 +502,7 @@ app.post('/uploadimage', function (req, res) {
                 switch (config.imageUploadType) {
                 case 'filesystem':
                     res.send({
-                        link: url.resolve(config.serverurl, files.image.path.match(/^public(.+$)/)[1])
+                        link: url.resolve(config.serverurl + '/', files.image.path.match(/^public\/(.+$)/)[1])
                     });
 
                     break;
@@ -608,7 +608,7 @@ function startListen() {
 // sync db then start listen
 models.sequelize.sync().then(function () {
     // check if realtime is ready
-    if (history.isReady() && realtime.isReady()) {
+    if (realtime.isReady()) {
         models.Revision.checkAllNotesRevision(function (err, notes) {
             if (err) throw new Error(err);
             if (!notes || notes.length <= 0) return startListen();
@@ -639,7 +639,7 @@ function handleTermSignals() {
         }, 0);
     });
     var checkCleanTimer = setInterval(function () {
-        if (history.isReady() && realtime.isReady()) {
+        if (realtime.isReady()) {
             models.Revision.checkAllNotesRevision(function (err, notes) {
                 if (err) return logger.error(err);
                 if (!notes || notes.length <= 0) {
diff --git a/bin/heroku b/bin/heroku
index 6ee7fa4..1228b28 100755
--- a/bin/heroku
+++ b/bin/heroku
@@ -28,8 +28,6 @@ EOF
 
 EOF
 
-  cp public/js/config.js.example public/js/config.js
-
   # build app
-  npm run build:prod
+  npm run build
 fi
diff --git a/bin/setup b/bin/setup
index e24d4de..3f143cd 100755
--- a/bin/setup
+++ b/bin/setup
@@ -1,5 +1,7 @@
 #!/bin/bash
 
+set -e
+
 # run command at repo root
 CURRENT_PATH=$PWD
 if [ -d .git ]; then
@@ -21,10 +23,6 @@ if [ ! -f config.json ]; then
   cp config.json.example config.json
 fi
 
-if [ ! -f publis/js/config.js ]; then
-  cp public/js/config.js.example public/js/config.js
-fi
-
 if [ ! -f .sequelizerc ]; then
   cp .sequelizerc.example .sequelizerc
 fi
diff --git a/config.json.example b/config.json.example
index 57669cf..9ee00c0 100644
--- a/config.json.example
+++ b/config.json.example
@@ -2,18 +2,13 @@
     "test": {
         "db": {
             "dialect": "sqlite",
-            "storage": "./db.hackmd.sqlite"
+            "storage": ":memory:"
         }
     },
     "development": {
-        "domain": "localhost",
         "db": {
-            "username": "",
-            "password": "",
-            "database": "hackmd",
-            "host": "localhost",
-            "port": "3306",
-            "dialect": "mysql"
+            "dialect": "sqlite",
+            "storage": "./db.hackmd.sqlite"
         }
     },
     "production": {
@@ -45,11 +40,13 @@
         },
         "dropbox": {
             "clientID": "change this",
-            "clientSecret": "change this"
+            "clientSecret": "change this",
+            "appKey": "change this"
         },
         "google": {
             "clientID": "change this",
-            "clientSecret": "change this"
+            "clientSecret": "change this",
+            "apiKey": "change this"
         },
         "ldap": {
             "url": "ldap://change_this",
diff --git a/lib/config.js b/lib/config.js
index ab2f67b..1e5838e 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -1,4 +1,5 @@
 // external modules
+var fs = require('fs');
 var path = require('path');
 var fs = require('fs');
 
@@ -27,8 +28,16 @@ var allowanonymous = process.env.HMD_ALLOW_ANONYMOUS ? (process.env.HMD_ALLOW_AN
 
 var allowfreeurl = process.env.HMD_ALLOW_FREEURL ? (process.env.HMD_ALLOW_FREEURL === 'true') : !!config.allowfreeurl;
 
+var permissions = ['editable', 'limited', 'locked', 'protected', 'private'];
+if (allowanonymous) {
+    permissions.unshift('freely');
+}
+
+var defaultpermission = process.env.HMD_DEFAULT_PERMISSION || config.defaultpermission;
+defaultpermission = permissions.indexOf(defaultpermission) != -1 ? defaultpermission : 'editable';
+
 // db
-var dburl = config.dburl || process.env.HMD_DB_URL || process.env.DATABASE_URL;
+var dburl = process.env.HMD_DB_URL || process.env.DATABASE_URL || config.dburl;
 var db = config.db || {};
 
 // ssl path
@@ -91,15 +100,16 @@ var gitlab = (process.env.HMD_GITLAB_CLIENTID && process.env.HMD_GITLAB_CLIENTSE
     clientID: handleDockerSecret('gitlab_clientID') || process.env.HMD_GITLAB_CLIENTID,
     clientSecret: handleDockerSecret('gitlab_clientSecret') || process.env.HMD_GITLAB_CLIENTSECRET
 } : config.gitlab || false;
-var dropbox = (process.env.HMD_DROPBOX_CLIENTID && process.env.HMD_DROPBOX_CLIENTSECRET || fs.existsSync('/run/secrets/dropbox_clientID') && fs.existsSync('/run/secrets/dropbox_clientSecret')) ? {
+var dropbox = ((process.env.HMD_DROPBOX_CLIENTID && process.env.HMD_DROPBOX_CLIENTSECRET) || (fs.existsSync('/run/secrets/dropbox_clientID') && fs.existsSync('/run/secrets/dropbox_clientSecret'))) ? {
     clientID: handleDockerSecret('dropbox_clientID') || process.env.HMD_DROPBOX_CLIENTID,
     clientSecret: handleDockerSecret('dropbox_clientSecret') || process.env.HMD_DROPBOX_CLIENTSECRET
-} : config.dropbox || false;
-var google = (process.env.HMD_GOOGLE_CLIENTID && process.env.HMD_GOOGLE_CLIENTSECRET || fs.existsSync('/run/secrets/google_clientID') && fs.existsSync('/run/secrets/google_clientSecret')) ? {
-    clientID: process.env.HMD_GOOGLE_CLIENTID,
-    clientSecret: process.env.HMD_GOOGLE_CLIENTSECRET
-} : config.google || false;
-var ldap = config.ldap || (
+} : (config.dropbox && config.dropbox.clientID && config.dropbox.clientSecret && config.dropbox) || false;
+var google = ((process.env.HMD_GOOGLE_CLIENTID && process.env.HMD_GOOGLE_CLIENTSECRET)
+              || (fs.existsSync('/run/secrets/google_clientID') && fs.existsSync('/run/secrets/google_clientSecret'))) ? {
+    clientID: handleDockerSecret('google_clientID') || process.env.HMD_GOOGLE_CLIENTID,
+    clientSecret: handleDockerSecret('google_clientSecret') || process.env.HMD_GOOGLE_CLIENTSECRET
+} : (config.google && config.google.clientID && config.google.clientSecret && config.google) || false;
+var ldap = config.ldap || ((
     process.env.HMD_LDAP_URL ||
     process.env.HMD_LDAP_BINDDN ||
     process.env.HMD_LDAP_BINDCREDENTIALS ||
@@ -107,10 +117,9 @@ var ldap = config.ldap || (
     process.env.HMD_LDAP_SEARCHBASE ||
     process.env.HMD_LDAP_SEARCHFILTER ||
     process.env.HMD_LDAP_SEARCHATTRIBUTES ||
+    process.env.HMD_LDAP_TLS_CA ||
     process.env.HMD_LDAP_PROVIDERNAME
-) || false;
-if (ldap == true)
-    ldap = {};
+) ? {} : false);
 if (process.env.HMD_LDAP_URL)
     ldap.url = process.env.HMD_LDAP_URL;
 if (process.env.HMD_LDAP_BINDDN)
@@ -127,9 +136,17 @@ if (process.env.HMD_LDAP_SEARCHATTRIBUTES)
     ldap.searchAttributes = process.env.HMD_LDAP_SEARCHATTRIBUTES;
 if (process.env.HMD_LDAP_TLS_CA) {
     var ca = {
-        ca: process.env.HMD_LDAP_TLS_CA
+        ca: process.env.HMD_LDAP_TLS_CA.split(',')
+    }
+    ldap.tlsOptions = ldap.tlsOptions ? Object.assign(ldap.tlsOptions, ca) : ca;
+    if (Array.isArray(ldap.tlsOptions.ca) && ldap.tlsOptions.ca.length > 0) {
+        var i, len, results;
+        results = [];
+        for (i = 0, len = ldap.tlsOptions.ca.length; i < len; i++) {
+            results.push(fs.readFileSync(ldap.tlsOptions.ca[i], 'utf8'));
+        }
+        ldap.tlsOptions.ca = results;
     }
-    ldap.tlsOptions = ldap.tlsOptions ? Object.assign(ldap.tlsOptions, ca) : ca
 }
 if (process.env.HMD_LDAP_PROVIDERNAME) {
     ldap.providerName = process.env.HMD_LDAP_PROVIDERNAME;
@@ -169,6 +186,7 @@ module.exports = {
     usecdn: usecdn,
     allowanonymous: allowanonymous,
     allowfreeurl: allowfreeurl,
+    defaultpermission: defaultpermission,
     dburl: dburl,
     db: db,
     sslkeypath: path.join(cwd, sslkeypath),
diff --git a/lib/history.js b/lib/history.js
index 2723422..e7fb308 100644
--- a/lib/history.js
+++ b/lib/history.js
@@ -1,7 +1,6 @@
 //history
 //external modules
 var async = require('async');
-var moment = require('moment');
 
 //core
 var config = require("./config.js");
@@ -14,45 +13,32 @@ var History = {
     historyGet: historyGet,
     historyPost: historyPost,
     historyDelete: historyDelete,
-    isReady: isReady,
     updateHistory: updateHistory
 };
 
-var caches = {};
-//update when the history is dirty
-var updater = setInterval(function () {
-    var deleted = [];
-    async.each(Object.keys(caches), function (key, callback) {
-        var cache = caches[key];
-        if (cache.isDirty) {
-            if (config.debug) logger.info("history updater found dirty history: " + key);
-            var history = parseHistoryToArray(cache.history);
-            cache.isDirty = false;
-            finishUpdateHistory(key, history, function (err, count) {
-                if (err) return callback(err, null);
-                if (!count) return callback(null, null);
-                cache.updateAt = Date.now();
-                return callback(null, null);
-            });
-        } else {
-            if (moment().isAfter(moment(cache.updateAt).add(5, 'minutes'))) {
-                deleted.push(key);
-            }
-            return callback(null, null);
+function getHistory(userid, callback) {
+    models.User.findOne({
+        where: {
+            id: userid
         }
-    }, function (err) {
-        if (err) return logger.error('history updater error', err);
+    }).then(function (user) {
+        if (!user)
+            return callback(null, null);
+        var history = {};
+        if (user.history)
+            history = parseHistoryToObject(JSON.parse(user.history));
+        if (config.debug)
+            logger.info('read history success: ' + user.id);
+        return callback(null, history);
+    }).catch(function (err) {
+        logger.error('read history failed: ' + err);
+        return callback(err, null);
     });
-    // delete specified caches
-    for (var i = 0, l = deleted.length; i < l; i++) {
-        caches[deleted[i]].history = {};
-        delete caches[deleted[i]];
-    }
-}, 1000);
+}
 
-function finishUpdateHistory(userid, history, callback) {
+function setHistory(userid, history, callback) {
     models.User.update({
-        history: JSON.stringify(history)
+        history: JSON.stringify(parseHistoryToArray(history))
     }, {
         where: {
             id: userid
@@ -60,72 +46,27 @@ function finishUpdateHistory(userid, history, callback) {
     }).then(function (count) {
         return callback(null, count);
     }).catch(function (err) {
+        logger.error('set history failed: ' + err);
         return callback(err, null);
     });
 }
 
-function isReady() {
-    var dirtyCount = 0;
-    async.each(Object.keys(caches), function (key, callback) {
-        if (caches[key].isDirty) dirtyCount++;
-        return callback(null, null);
-    }, function (err) {
-        if (err) return logger.error('history ready check error', err);
-    });
-    return dirtyCount > 0 ? false : true;
-}
-
-function getHistory(userid, callback) {
-    if (caches[userid]) {
-        return callback(null, caches[userid].history);
-    } else {
-        models.User.findOne({
-            where: {
-                id: userid
-            }
-        }).then(function (user) {
-            if (!user)
-                return callback(null, null);
-            var history = [];
-            if (user.history)
-                history = JSON.parse(user.history);
-            if (config.debug)
-                logger.info('read history success: ' + user.id);
-            setHistory(userid, history);
-            return callback(null, history);
-        }).catch(function (err) {
-            logger.error('read history failed: ' + err);
-            return callback(err, null);
-        });   
-    }
-}
-
-function setHistory(userid, history) {
-    if (Array.isArray(history)) history = parseHistoryToObject(history);
-    if (!caches[userid]) {
-        caches[userid] = {
-            history: {},
-            isDirty: false,
-            updateAt: Date.now()
-        };
-    }
-    caches[userid].history = history;
-}
-
-function updateHistory(userid, noteId, document) {
+function updateHistory(userid, noteId, document, time) {
     if (userid && noteId && typeof document !== 'undefined') {
         getHistory(userid, function (err, history) {
             if (err || !history) return;
-            if (!caches[userid].history[noteId]) {
-                caches[userid].history[noteId] = {};
+            if (!history[noteId]) {
+                history[noteId] = {};
             }
-            var noteHistory = caches[userid].history[noteId];
+            var noteHistory = history[noteId];
             var noteInfo = models.Note.parseNoteInfo(document);
             noteHistory.id = noteId;
             noteHistory.text = noteInfo.title;
-            noteHistory.time = moment().valueOf();
+            noteHistory.time = time || Date.now();
             noteHistory.tags = noteInfo.tags;
-            caches[userid].isDirty = true;
+            setHistory(userid, history, function (err, count) {
+                return;
+            });
         });
     }
 }
@@ -175,9 +116,10 @@ function historyPost(req, res) {
                 return response.errorBadRequest(res);
             }
             if (Array.isArray(history)) {
-                setHistory(req.user.id, history);
-                caches[req.user.id].isDirty = true;
-                res.end();
+                setHistory(req.user.id, history, function (err, count) {
+                    if (err) return response.errorInternalError(res);
+                    res.end();
+                });
             } else {
                 return response.errorBadRequest(res);
             }
@@ -186,11 +128,13 @@ function historyPost(req, res) {
             getHistory(req.user.id, function (err, history) {
                 if (err) return response.errorInternalError(res);
                 if (!history) return response.errorNotFound(res);
-                if (!caches[req.user.id].history[noteId]) return response.errorNotFound(res);
+                if (!history[noteId]) return response.errorNotFound(res);
                 if (req.body.pinned === 'true' || req.body.pinned === 'false') {
-                    caches[req.user.id].history[noteId].pinned = (req.body.pinned === 'true');
-                    caches[req.user.id].isDirty = true;
-                    res.end();
+                    history[noteId].pinned = (req.body.pinned === 'true');
+                    setHistory(req.user.id, history, function (err, count) {
+                        if (err) return response.errorInternalError(res);
+                        res.end();
+                    });
                 } else {
                     return response.errorBadRequest(res);
                 }
@@ -205,16 +149,19 @@ function historyDelete(req, res) {
     if (req.isAuthenticated()) {
         var noteId = req.params.noteId;
         if (!noteId) {
-            setHistory(req.user.id, []);
-            caches[req.user.id].isDirty = true;
-            res.end();
+            setHistory(req.user.id, [], function (err, count) {
+                if (err) return response.errorInternalError(res);
+                res.end();
+            });
         } else {
             getHistory(req.user.id, function (err, history) {
                 if (err) return response.errorInternalError(res);
                 if (!history) return response.errorNotFound(res);
-                delete caches[req.user.id].history[noteId];
-                caches[req.user.id].isDirty = true;
-                res.end();
+                delete history[noteId];
+                setHistory(req.user.id, history, function (err, count) {
+                    if (err) return response.errorInternalError(res);
+                    res.end();
+                });
             });
         }
     } else {
diff --git a/lib/migrations/20161009040430-support-delete-note.js b/lib/migrations/20161009040430-support-delete-note.js
index f478b6f..92ff6f7 100644
--- a/lib/migrations/20161009040430-support-delete-note.js
+++ b/lib/migrations/20161009040430-support-delete-note.js
@@ -6,6 +6,6 @@ module.exports = {
   },
 
   down: function (queryInterface, Sequelize) {
-    queryInterface.removeColumn('Notes', 'deletedAt', Sequelize.DATE);
+    queryInterface.removeColumn('Notes', 'deletedAt');
   }
 };
diff --git a/lib/migrations/20161201050312-support-email-signin.js b/lib/migrations/20161201050312-support-email-signin.js
index bdea7c8..b5aaf77 100644
--- a/lib/migrations/20161201050312-support-email-signin.js
+++ b/lib/migrations/20161201050312-support-email-signin.js
@@ -7,7 +7,7 @@ module.exports = {
   },
 
   down: function (queryInterface, Sequelize) {
-    queryInterface.removeColumn('Users', 'email', Sequelize.TEXT);
-    queryInterface.removeColumn('Users', 'password', Sequelize.TEXT);
+    queryInterface.removeColumn('Users', 'email');
+    queryInterface.removeColumn('Users', 'password');
   }
 };
diff --git a/lib/models/note.js b/lib/models/note.js
index 8611297..8b38d3f 100644
--- a/lib/models/note.js
+++ b/lib/models/note.js
@@ -513,10 +513,10 @@ module.exports = function (sequelize, DataTypes) {
                         }
                     }
                 }
-                // if no permission specified and have owner then give editable permission, else default permission is freely
+                // if no permission specified and have owner then give default permission in config, else default permission is freely
                 if (!note.permission) {
                     if (note.ownerId) {
-                        note.permission = "editable";
+                        note.permission = config.defaultpermission;
                     } else {
                         note.permission = "freely";
                     }
diff --git a/lib/models/user.js b/lib/models/user.js
index 7d27242..dd93bf7 100644
--- a/lib/models/user.js
+++ b/lib/models/user.js
@@ -79,39 +79,54 @@ module.exports = function (sequelize, DataTypes) {
                 if (profile) {
                     profile = {
                         name: profile.displayName || profile.username,
-                        photo: User.parsePhotoByProfile(profile)
+                        photo: User.parsePhotoByProfile(profile),
+                        biggerphoto: User.parsePhotoByProfile(profile, true)
                     }
                 }
                 return profile;
             },
-            parsePhotoByProfile: function (profile) {
+            parsePhotoByProfile: function (profile, bigger) {
                 var photo = null;
                 switch (profile.provider) {
                     case "facebook":
-                        photo = 'https://graph.facebook.com/' + profile.id + '/picture?width=96';
+                        photo = 'https://graph.facebook.com/' + profile.id + '/picture';
+                        if (bigger) photo += '?width=400';
+                        else photo += '?width=96';
                         break;
                     case "twitter":
-                        photo = 'https://twitter.com/' + profile.username + '/profile_image?size=bigger';
+                        photo = 'https://twitter.com/' + profile.username + '/profile_image';
+                        if (bigger) photo += '?size=original';
+                        else photo += '?size=bigger';
                         break;
                     case "github":
-                        photo = 'https://avatars.githubusercontent.com/u/' + profile.id + '?s=96';
+                        photo = 'https://avatars.githubusercontent.com/u/' + profile.id;
+                        if (bigger) photo += '?s=400';
+                        else photo += '?s=96';
                         break;
                     case "gitlab":
-                        photo = profile.avatarUrl.replace(/(\?s=)\d*$/i, '$196');
+                        photo = profile.avatarUrl;
+                        if (bigger) photo = photo.replace(/(\?s=)\d*$/i, '$1400');
+                        else photo = photo.replace(/(\?s=)\d*$/i, '$196');
                         break;
                     case "dropbox":
                         //no image api provided, use gravatar
-                        photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0].value) + '?s=96';
+                        photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0].value);
+                        if (bigger) photo += '?s=400';
+                        else photo += '?s=96';
                         break;
                     case "google":
-                        photo = profile.photos[0].value.replace(/(\?sz=)\d*$/i, '$196');
+                        photo = profile.photos[0].value;
+                        if (bigger) photo = photo.replace(/(\?sz=)\d*$/i, '$1400');
+                        else photo = photo.replace(/(\?sz=)\d*$/i, '$196');
                         break;
                     case "ldap":
                         //no image api provided,
                         //use gravatar if email exists,
                         //otherwise generate a letter avatar
                         if (profile.emails[0]) {
-                            photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0]) + '?s=96';
+                            photo = 'https://www.gravatar.com/avatar/' + md5(profile.emails[0]);
+                            if (bigger) photo += '?s=400';
+                            else photo += '?s=96';
                         } else {
                             photo = letterAvatars(profile.username);
                         }
@@ -123,7 +138,8 @@ module.exports = function (sequelize, DataTypes) {
                 var photoUrl = 'https://www.gravatar.com/avatar/' + md5(email);
                 return {
                     name: email.substring(0, email.lastIndexOf("@")),
-                    photo: photoUrl += '?s=96'
+                    photo: photoUrl += '?s=96',
+                    biggerphoto: photoUrl += '?s=400'
                 };
             }
         }
diff --git a/lib/realtime.js b/lib/realtime.js
index fadea4f..c1db688 100644
--- a/lib/realtime.js
+++ b/lib/realtime.js
@@ -122,6 +122,12 @@ function updateNote(note, callback) {
         }
     }).then(function (_note) {
         if (!_note) return callback(null, null);
+        // update user note history
+        var tempUsers = Object.assign({}, note.tempUsers);
+        note.tempUsers = {};
+        Object.keys(tempUsers).forEach(function (key) {
+            updateHistory(key, note, tempUsers[key]);
+        });
         if (note.lastchangeuser) {
             if (_note.lastchangeuserId != note.lastchangeuser) {
                 models.User.findOne({
@@ -348,9 +354,12 @@ function clearSocketQueue(queue, socket) {
 }
 
 function connectNextSocket() {
-    isConnectionBusy = false;
-    if (connectionSocketQueue.length > 0)
-        startConnection(connectionSocketQueue[0]);
+    setTimeout(function () {
+        isConnectionBusy = false;
+        if (connectionSocketQueue.length > 0) {
+            startConnection(connectionSocketQueue[0]);
+        }
+    }, 1);
 }
 
 function interruptConnection(socket, note, user) {
@@ -405,10 +414,7 @@ function finishConnection(socket, note, user) {
     note.server.setColor(socket, user.color);
 
     // update user note history
-    setTimeout(function () {
-        var noteId = note.alias ? note.alias : LZString.compressToBase64(note.id);
-        if (note.server) history.updateHistory(user.userid, noteId, note.server.document);
-    }, 0);
+    updateHistory(user.userid, note);
 
     emitOnlineUsers(socket);
     emitRefresh(socket);
@@ -497,6 +503,7 @@ function startConnection(socket) {
                 lastchangeuserprofile: lastchangeuserprofile,
                 socks: [],
                 users: {},
+                tempUsers: {},
                 createtime: moment(createtime).valueOf(),
                 updatetime: moment(updatetime).valueOf(),
                 server: server,
@@ -687,15 +694,17 @@ function operationCallback(socket, operation) {
                 return logger.error('operation callback failed: ' + err);
             });
         }
-        // update user note history
-        setTimeout(function() {
-            var noteId = note.alias ? note.alias : LZString.compressToBase64(note.id);
-            if (note.server) history.updateHistory(userId, noteId, note.server.document);
-        }, 0);
-
+        note.tempUsers[userId] = Date.now();
     }
-    // save authorship
-    note.authorship = models.Note.updateAuthorshipByOperation(operation, userId, note.authorship);
+    // save authorship - use timer here because it's an O(n) complexity algorithm
+    setImmediate(function () {
+        note.authorship = models.Note.updateAuthorshipByOperation(operation, userId, note.authorship);
+    });
+}
+
+function updateHistory(userId, note, time) {
+    var noteId = note.alias ? note.alias : LZString.compressToBase64(note.id);
+    if (note.server) history.updateHistory(userId, noteId, note.server.document, time);
 }
 
 function connection(socket) {
@@ -925,4 +934,4 @@ function connection(socket) {
     });
 }
 
-module.exports = realtime;
+module.exports = realtime;
\ No newline at end of file
diff --git a/locales/de.json b/locales/de.json
index 4d8ba05..41d8977 100644
--- a/locales/de.json
+++ b/locales/de.json
@@ -47,7 +47,7 @@
 	"View": "Anzeigen",
 	"Both": "Beides",
 	"Help": "Hilfe",
-	"Upload Image": "Foto hochloaden",
+	"Upload Image": "Foto hochladen",
 	"Menu": "Menü",
 	"This page need refresh": "Bitte laden Sie die Seite neu",
 	"You have an incompatible client version.": "Ihre Client Version ist nicht mit dem Server kompatibel",
diff --git a/locales/el.json b/locales/el.json
index ea68aac..8f65dfa 100644
--- a/locales/el.json
+++ b/locales/el.json
@@ -1,7 +1,7 @@
 {
 	"Collaborative markdown notes": "Συνεργατικές σημειώσεις markdown",
 	"Realtime collaborative markdown notes on all platforms.": "Συνεργατική σημειώσεις markdown σε όλες τις πλατφόρμες σε πραγματικό χρόνο.",
-	"Best way to write and share your knowledge in markdown.": "Ο καλύτερος τρόπς να γράψεις και να μοιρατσείς την γνώση σου in markdown.",
+	"Best way to write and share your knowledge in markdown.": "Ο καλύτερος τρόπος να γράψεις και να μοιραστείς την γνώση σου σε markdown.",
 	"Intro": "Εισαγωγή",
 	"History": "Ιστορία",
 	"New guest note": "Νέα σημείωση επισκέπτη",
@@ -54,9 +54,9 @@
 	"Refresh to update.": "Ανανεώστε για ενημέρωση",
 	"New version available!": "Νέα διαθέσιμη έκδοση ",
 	"See releases notes here": "Δείτε τις κυκλοφορίες της σημείωσης εδώ",
-	"Refresh to enjoy new features.": "Ανανεώστε για να δείτε τις κανούργίες λειτουργίες",
+	"Refresh to enjoy new features.": "Ανανεώστε για να δείτε τις κανούργιες λειτουργίες",
 	"Your user state has changed.": "Η κατάσταση χρήστη έχει αλλάξει.",
-	"Refresh to load new user state.": "Ανανεώστε για να φορτωσετε την νέα κατάσταση χρήστη.",
+	"Refresh to load new user state.": "Ανανεώστε για να φορτώσετε την νέα κατάσταση χρήστη.",
 	"Refresh": "Ανανέωση",
 	"Contacts": "Επαφές",
 	"Report an issue": "Αναφέρετε ένα θέμα",
@@ -73,32 +73,32 @@
 	"Ordered List": "Αριθμημένη λίστα",
 	"Todo List": "Todo List",
 	"Blockquote": "Παράγραφος",
-	"Bold font": "Εντονη γραμματόσειρά",
-	"Italics font": "Italics γραμματόσειρά",
-	"Strikethrough": "Διακριτή διαγραφή",
+	"Bold font": "Εντονη γραμματοσειρά",
+	"Italics font": "Πλάγια γραμματοσειρά",
+	"Strikethrough": "Διαγραμένη γραμματοσειρά",
 	"Inserted text": "Εισαγμένο κείμενο",
-	"Marked text": "Επιλεγμένο κέιμενο",
+	"Marked text": "Επιλεγμένο κείμενο",
 	"Link": "Σύνδεσμος",
 	"Image": "Εικόνα",
 	"Code": "Κώδικας",
 	"Externals": "Εξωτερικά",
-	"This is a alert area.": "Αυτή είνια μια περιοχή ειδοποίησης",
+	"This is a alert area.": "Αυτή είναι μια περιοχή ειδοποίησης",
 	"Revert": "Επαναστροφή",
 	"Import from clipboard": "Εισαγωγή από πρόχειρο",
-	"Paste your markdown or webpage here...": "Επικολλήστε το markdown ή την ιστοσελίδα σας εδώ...",
+	"Paste your markdown or webpage here...": "Επικολλήστε markdown ή την ιστοσελίδα σας εδώ...",
 	"Clear": "Καθαρισμός",
 	"This note is locked": "Η σημείωση είναι κλειδωμένη",
-	"Sorry, only owner can edit this note.": "Συγνώμη, μόνο ο ιδικτήτης μπορεί να επεξεργαστεί αυτη την σημείωση.",
+	"Sorry, only owner can edit this note.": "Συγνώμη, μόνο ο ιδιοκτήτης μπορεί να επεξεργαστεί αυτη την σημείωση.",
 	"OK": "Εντάξει",
 	"Reach the limit": "Φτάσατε το όριο",
 	"Sorry, you've reached the max length this note can be.": "Συγνώμη, φτάσατε το μέγιστο μέγεθος αυτής της σημείωσης.",
-	"Please reduce the content or divide it to more notes, thank you!": "Παρακαλώ μειώστε το περιεχόμενο η διαιρεστε το σε περισσότερες σημειώσεις, ευχαριστώ!",
+	"Please reduce the content or divide it to more notes, thank you!": "Παρακαλώ μειώστε το περιεχόμενο η διαιρέστε το σε περισσότερες σημειώσεις, ευχαριστώ!",
 	"Import from Gist": "Εισαγωγή από Gist",
 	"Paste your gist url here...": "Κάντε επικκόληση του gist url εδώ...",
-	"Import from Snippet": "Εισαγωγή απο Snippet",
+	"Import from Snippet": "Εισαγωγή από Snippet",
 	"Select From Available Projects": "Eπιλογή από διαθέσιμα Projects",
 	"Select From Available Snippets": "Eπιλογή από διαθέσιμα Snippets",
 	"OR": "Ή",
 	"Export to Snippet": "Eξαγωγή σε Snippet",
 	"Select Visibility Level": "Επιλέξτε επίπεδο ορατότητας"
-}
\ No newline at end of file
+}
diff --git a/package.json b/package.json
index a13ac97..a179d93 100644
--- a/package.json
+++ b/package.json
@@ -5,8 +5,10 @@
   "main": "app.js",
   "license": "MIT",
   "scripts": {
-    "build:dev": "webpack --config webpack.config.js --progress --colors --watch",
-    "build:prod": "webpack --config webpack.production.js --progress --colors",
+    "test": "npm run-script lint",
+    "lint": "eslint .",
+    "dev": "webpack --config webpack.config.js --progress --colors --watch",
+    "build": "webpack --config webpack.production.js --progress --colors",
     "postinstall": "bin/heroku",
     "start": "node app.js"
   },
@@ -71,7 +73,7 @@
     "markdown-it-sup": "^1.0.0",
     "markdown-pdf": "^7.0.0",
     "mathjax": "~2.7.0",
-    "mermaid": "~6.0.0",
+    "mermaid": "~7.0.0",
     "meta-marked": "^0.4.2",
     "method-override": "^2.3.7",
     "moment": "^2.17.1",
@@ -115,12 +117,12 @@
     "validator": "^6.2.0",
     "velocity-animate": "^1.4.0",
     "visibilityjs": "^1.2.4",
-    "viz.js": "^1.4.1",
+    "viz.js": "^1.7.0",
     "winston": "^2.3.0",
     "xss": "^0.3.3"
   },
   "engines": {
-    "node": ">=4.x"
+    "node": ">=6.x"
   },
   "bugs": "https://github.com/hackmdio/hackmd/issues",
   "keywords": [
@@ -140,9 +142,17 @@
     "url": "https://github.com/hackmdio/hackmd.git"
   },
   "devDependencies": {
+    "babel-cli": "^6.18.0",
+    "babel-core": "^6.21.0",
+    "babel-loader": "^6.2.10",
+    "babel-plugin-transform-runtime": "^6.15.0",
+    "babel-polyfill": "^6.22.0",
+    "babel-preset-es2015": "^6.18.0",
+    "babel-runtime": "^6.20.0",
     "copy-webpack-plugin": "^4.0.1",
     "css-loader": "^0.26.1",
     "ejs-loader": "^0.3.0",
+    "eslint": "^3.15.0",
     "exports-loader": "^0.6.3",
     "expose-loader": "^0.7.1",
     "extract-text-webpack-plugin": "^1.0.1",
@@ -156,6 +166,7 @@
     "script-loader": "^0.7.0",
     "style-loader": "^0.13.1",
     "url-loader": "^0.5.7",
-    "webpack": "^1.14.0"
+    "webpack": "^1.14.0",
+    "webpack-parallel-uglify-plugin": "^0.2.0"
   }
 }
diff --git a/public/css/cover.css b/public/css/cover.css
index a1527bf..6e191d2 100644
--- a/public/css/cover.css
+++ b/public/css/cover.css
@@ -354,6 +354,12 @@ input {
     color: white;
 }
 
+.screenshot {
+    margin: 30px auto;
+    width: 100%;
+    border-radius: 3px;
+}
+
 select {
     color: black;
 }
diff --git a/public/docs/features.md b/public/docs/features.md
index 1c25bfe..1e9d48f 100644
--- a/public/docs/features.md
+++ b/public/docs/features.md
@@ -49,7 +49,9 @@ There are four possible options:
 
 <i class="fa fa-leaf fa-fw"></i> **Freely**: Anyone can edit this note.
 <i class="fa fa-pencil fa-fw"></i> **Editable**: A signed-in user can edit this note.
-<i class="fa fa-lock fa-fw"></i> **Locked**: Only the owner can edit this note.
+<i class="fa fa-id-card fa-fw"></i> **Limited**: People have to sign-in to view and edit this note.
+<i class="fa fa-lock fa-fw"></i> **Locked**: Anyone can view this note but only the owner can edit it.
+<i class="fa fa-umbrella fa-fw"></i> **Protected**: People have to sign-in to view this note but only owner can edit.
 <i class="fa fa-hand-stop-o fa-fw"></i> **Private**: Only the owner can view and edit this note.
 
 **Only the owner of the note can change the note's permissions.**
diff --git a/public/js/common.js b/public/js/common.js
deleted file mode 100644
index 7eee107..0000000
--- a/public/js/common.js
+++ /dev/null
@@ -1,118 +0,0 @@
-var config = require('./config');
-var domain = config.domain; // domain name
-var urlpath = config.urlpath; // sub url path, like: www.example.com/<urlpath>
-var debug = config.debug;
-var GOOGLE_API_KEY = config.GOOGLE_API_KEY;
-var GOOGLE_CLIENT_ID = config.GOOGLE_CLIENT_ID;
-var DROPBOX_APP_KEY = config.DROPBOX_APP_KEY;
-
-//common
-var port = window.location.port;
-window.serverurl = window.location.protocol + '//' + (domain ? domain : window.location.hostname) + (port ? ':' + port : '') + (urlpath ? '/' + urlpath : '');
-var noteid = urlpath ? window.location.pathname.slice(urlpath.length + 1, window.location.pathname.length).split('/')[1] : window.location.pathname.split('/')[1];
-var noteurl = serverurl + '/' + noteid;
-
-var version = '0.5.0';
-
-var checkAuth = false;
-var profile = null;
-var lastLoginState = getLoginState();
-var lastUserId = getUserId();
-
-window.loginStateChangeEvent = null;
-
-function resetCheckAuth() {
-    checkAuth = false;
-}
-
-function setLoginState(bool, id) {
-    Cookies.set('loginstate', bool, {
-        expires: 365
-    });
-    if (id) {
-        Cookies.set('userid', id, {
-            expires: 365
-        });
-    } else {
-        Cookies.remove('userid');
-    }
-    lastLoginState = bool;
-    lastUserId = id;
-    checkLoginStateChanged();
-}
-
-function checkLoginStateChanged() {
-    if (getLoginState() != lastLoginState || getUserId() != lastUserId) {
-        if(loginStateChangeEvent) setTimeout(loginStateChangeEvent, 100);
-        return true;
-    } else {
-        return false;
-    }
-}
-
-function getLoginState() {
-    var state = Cookies.get('loginstate');
-    return state === "true" || state === true;
-}
-
-function getUserId() {
-    return Cookies.get('userid');
-}
-
-function clearLoginState() {
-    Cookies.remove('loginstate');
-}
-
-function checkIfAuth(yesCallback, noCallback) {
-    var cookieLoginState = getLoginState();
-    if (checkLoginStateChanged()) checkAuth = false;
-    if (!checkAuth || typeof cookieLoginState == 'undefined') {
-        $.get(serverurl + '/me')
-            .done(function (data) {
-                if (data && data.status == 'ok') {
-                    profile = data;
-                    yesCallback(profile);
-                    setLoginState(true, data.id);
-                } else {
-                    noCallback();
-                    setLoginState(false);
-                }
-            })
-            .fail(function () {
-                noCallback();
-            })
-            .always(function () {
-                checkAuth = true;
-            });
-    } else if (cookieLoginState) {
-        yesCallback(profile);
-    } else {
-        noCallback();
-    }
-}
-
-module.exports = {
-    domain: domain,
-    urlpath: urlpath,
-    debug: debug,
-    GOOGLE_API_KEY: GOOGLE_API_KEY,
-    GOOGLE_CLIENT_ID: GOOGLE_CLIENT_ID,
-    DROPBOX_APP_KEY: DROPBOX_APP_KEY,
-    port: port,
-    noteid: noteid,
-    noteurl: noteurl,
-    version: version,
-    checkAuth: checkAuth,
-    profile: profile,
-    lastLoginState: lastLoginState,
-    lastUserId: lastUserId,
-
-    /* export functions */
-    resetCheckAuth: resetCheckAuth,
-    setLoginState: setLoginState,
-    checkLoginStateChanged: checkLoginStateChanged,
-    getLoginState: getLoginState,
-    getUserId: getUserId,
-    clearLoginState: clearLoginState,
-    checkIfAuth: checkIfAuth
-};
diff --git a/public/js/config.js.example b/public/js/config.js.example
deleted file mode 100644
index c5de388..0000000
--- a/public/js/config.js.example
+++ /dev/null
@@ -1,11 +0,0 @@
-module.exports = {
-    domain: '', // domain name
-    urlpath: '', // sub url path, like: www.example.com/<urlpath>
-
-    // settings
-    debug: false,
-
-    GOOGLE_API_KEY: '',
-    GOOGLE_CLIENT_ID: '',
-    DROPBOX_APP_KEY: ''
-};
diff --git a/public/js/cover.js b/public/js/cover.js
index f8ffe9b..bc6e73f 100644
--- a/public/js/cover.js
+++ b/public/js/cover.js
@@ -3,37 +3,39 @@ require('./locale');
 require('../css/cover.css');
 require('../css/site.css');
 
-var common = require('./common');
-var checkIfAuth = common.checkIfAuth;
-var urlpath = common.urlpath;
-var resetCheckAuth = common.resetCheckAuth;
-var getLoginState = common.getLoginState;
-var clearLoginState = common.clearLoginState;
+import {
+    checkIfAuth,
+    clearLoginState,
+    getLoginState,
+    resetCheckAuth,
+    setloginStateChangeEvent
+} from './lib/common/login';
 
-var historyModule = require('./history');
-var parseStorageToHistory = historyModule.parseStorageToHistory;
-var parseHistory = historyModule.parseHistory;
-var getStorageHistory = historyModule.getStorageHistory;
-var getHistory = historyModule.getHistory;
-var saveHistory = historyModule.saveHistory;
-var removeHistory = historyModule.removeHistory;
-var postHistoryToServer = historyModule.postHistoryToServer;
-var deleteServerHistory = historyModule.deleteServerHistory;
-var parseServerToHistory = historyModule.parseServerToHistory;
-var saveStorageHistoryToServer = historyModule.saveStorageHistoryToServer;
-var clearDuplicatedHistory = historyModule.clearDuplicatedHistory;
+import {
+    clearDuplicatedHistory,
+    deleteServerHistory,
+    getHistory,
+    getStorageHistory,
+    parseHistory,
+    parseServerToHistory,
+    parseStorageToHistory,
+    postHistoryToServer,
+    removeHistory,
+    saveHistory,
+    saveStorageHistoryToServer
+} from './history';
 
-var saveAs = require('file-saver').saveAs;
-var List = require('list.js');
-var S = require('string');
+import { saveAs } from 'file-saver';
+import List from 'list.js';
+import S from 'string';
 
-var options = {
+const options = {
     valueNames: ['id', 'text', 'timestamp', 'fromNow', 'time', 'tags', 'pinned'],
     item: '<li class="col-xs-12 col-sm-6 col-md-6 col-lg-4">\
             <span class="id" style="display:none;"></span>\
             <a href="#">\
                 <div class="item">\
-					<div class="ui-history-pin fa fa-thumb-tack fa-fw"></div>\
+                    <div class="ui-history-pin fa fa-thumb-tack fa-fw"></div>\
                     <div class="ui-history-close fa fa-close fa-fw" data-toggle="modal" data-target=".delete-modal"></div>\
                     <div class="content">\
                         <h4 class="text"></h4>\
@@ -55,15 +57,16 @@ var options = {
         })
     ]
 };
-var historyList = new List('history', options);
+const historyList = new List('history', options);
 
 migrateHistoryFromTempCallback = pageInit;
-loginStateChangeEvent = pageInit;
+setloginStateChangeEvent(pageInit);
+
 pageInit();
 
 function pageInit() {
     checkIfAuth(
-        function (data) {
+        data => {
             $('.ui-signin').hide();
             $('.ui-or').hide();
             $('.ui-welcome').show();
@@ -74,7 +77,7 @@ function pageInit() {
             $(".ui-history").click();
             parseServerToHistory(historyList, parseHistoryCallback);
         },
-        function () {
+        () => {
             $('.ui-signin').show();
             $('.ui-or').show();
             $('.ui-welcome').hide();
@@ -103,7 +106,7 @@ $(".ui-home").click(function (e) {
     }
 });
 
-$(".ui-history").click(function (e) {
+$(".ui-history").click(() => {
     if (!$("#history").is(':visible')) {
         $(".section:visible").hide();
         $("#history").fadeIn();
@@ -118,7 +121,7 @@ function checkHistoryList() {
     } else if ($("#history-list").children().length == 0) {
         $('.pagination').hide();
         $(".ui-nohistory").slideDown();
-        getStorageHistory(function (data) {
+        getStorageHistory(data => {
             if (data && data.length > 0 && getLoginState() && historyList.items.length == 0) {
                 $(".ui-import-from-browser").slideDown();
             }
@@ -128,35 +131,35 @@ function checkHistoryList() {
 
 function parseHistoryCallback(list, notehistory) {
     checkHistoryList();
-	//sort by pinned then timestamp
-	list.sort('', {
-        sortFunction: function (a, b) {
-			var notea = a.values();
-            var noteb = b.values();
-			if (notea.pinned && !noteb.pinned) {
+    //sort by pinned then timestamp
+    list.sort('', {
+        sortFunction(a, b) {
+            const notea = a.values();
+            const noteb = b.values();
+            if (notea.pinned && !noteb.pinned) {
                 return -1;
             } else if (!notea.pinned && noteb.pinned) {
                 return 1;
             } else {
-				if (notea.timestamp > noteb.timestamp) {
-                	return -1;
-				} else if (notea.timestamp < noteb.timestamp) {
-					return 1;
-				} else {
-					return 0;
-				}
-			}
-		}
-	});
+                if (notea.timestamp > noteb.timestamp) {
+                    return -1;
+                } else if (notea.timestamp < noteb.timestamp) {
+                    return 1;
+                } else {
+                    return 0;
+                }
+            }
+        }
+    });
     // parse filter tags
-    var filtertags = [];
-    for (var i = 0, l = list.items.length; i < l; i++) {
-        var tags = list.items[i]._values.tags;
+    const filtertags = [];
+    for (let i = 0, l = list.items.length; i < l; i++) {
+        const tags = list.items[i]._values.tags;
         if (tags && tags.length > 0) {
-            for (var j = 0; j < tags.length; j++) {
+            for (let j = 0; j < tags.length; j++) {
                 //push info filtertags if not found
-                var found = false;
-                if (filtertags.indexOf(tags[j]) != -1)
+                let found = false;
+                if (filtertags.includes(tags[j]))
                     found = true;
                 if (!found)
                     filtertags.push(tags[j]);
@@ -167,17 +170,17 @@ function parseHistoryCallback(list, notehistory) {
 }
 
 // update items whenever list updated
-historyList.on('updated', function (e) {
-    for (var i = 0, l = e.items.length; i < l; i++) {
-        var item = e.items[i];
+historyList.on('updated', e => {
+    for (let i = 0, l = e.items.length; i < l; i++) {
+        const item = e.items[i];
         if (item.visible()) {
-            var itemEl = $(item.elm);
-            var values = item._values;
-            var a = itemEl.find("a");
-            var pin = itemEl.find(".ui-history-pin");
-            var tagsEl = itemEl.find(".tags");
+            const itemEl = $(item.elm);
+            const values = item._values;
+            const a = itemEl.find("a");
+            const pin = itemEl.find(".ui-history-pin");
+            const tagsEl = itemEl.find(".tags");
             //parse link to element a
-            a.attr('href', serverurl + '/' + values.id);
+            a.attr('href', `${serverurl}/${values.id}`);
             //parse pinned
             if (values.pinned) {
                 pin.addClass('active');
@@ -185,12 +188,12 @@ historyList.on('updated', function (e) {
                 pin.removeClass('active');
             }
             //parse tags
-            var tags = values.tags;
+            const tags = values.tags;
             if (tags && tags.length > 0 && tagsEl.children().length <= 0) {
-                var labels = [];
-                for (var j = 0; j < tags.length; j++) {
+                const labels = [];
+                for (let j = 0; j < tags.length; j++) {
                     //push into the item label
-                    labels.push("<span class='label label-default'>" + tags[j] + "</span>");
+                    labels.push(`<span class='label label-default'>${tags[j]}</span>`);
                 }
                 tagsEl.html(labels.join(' '));
             }
@@ -204,21 +207,21 @@ historyList.on('updated', function (e) {
 
 function historyCloseClick(e) {
     e.preventDefault();
-    var id = $(this).closest("a").siblings("span").html();
-    var value = historyList.get('id', id)[0]._values;
+    const id = $(this).closest("a").siblings("span").html();
+    const value = historyList.get('id', id)[0]._values;
     $('.ui-delete-modal-msg').text('Do you really want to delete below history?');
-    $('.ui-delete-modal-item').html('<i class="fa fa-file-text"></i> ' + value.text + '<br><i class="fa fa-clock-o"></i> ' + value.time);
+    $('.ui-delete-modal-item').html(`<i class="fa fa-file-text"></i> ${value.text}<br><i class="fa fa-clock-o"></i> ${value.time}`);
     clearHistory = false;
     deleteId = id;
 }
 
 function historyPinClick(e) {
     e.preventDefault();
-    var $this = $(this);
-    var id = $this.closest("a").siblings("span").html();
-    var item = historyList.get('id', id)[0];
-    var values = item._values;
-    var pinned = values.pinned;
+    const $this = $(this);
+    const id = $this.closest("a").siblings("span").html();
+    const item = historyList.get('id', id)[0];
+    const values = item._values;
+    let pinned = values.pinned;
     if (!values.pinned) {
         pinned = true;
         item._values.pinned = true;
@@ -226,10 +229,10 @@ function historyPinClick(e) {
         pinned = false;
         item._values.pinned = false;
     }
-    checkIfAuth(function () {
+    checkIfAuth(() => {
         postHistoryToServer(id, {
-            pinned: pinned
-        }, function (err, result) {
+            pinned
+        }, (err, result) => {
             if (!err) {
                 if (pinned)
                     $this.addClass('active');
@@ -237,9 +240,9 @@ function historyPinClick(e) {
                     $this.removeClass('active');
             }
         });
-    }, function () {
-        getHistory(function (notehistory) {
-            for(var i = 0; i < notehistory.length; i++) {
+    }, () => {
+        getHistory(notehistory => {
+            for(let i = 0; i < notehistory.length; i++) {
                 if (notehistory[i].id == id) {
                     notehistory[i].pinned = pinned;
                     break;
@@ -258,10 +261,10 @@ function historyPinClick(e) {
 setInterval(updateItemFromNow, 60000);
 
 function updateItemFromNow() {
-    var items = $('.item').toArray();
-    for (var i = 0; i < items.length; i++) {
-        var item = $(items[i]);
-        var timestamp = parseInt(item.find('.timestamp').text());
+    const items = $('.item').toArray();
+    for (let i = 0; i < items.length; i++) {
+        const item = $(items[i]);
+        const timestamp = parseInt(item.find('.timestamp').text());
         item.find('.fromNow').text(moment(timestamp).fromNow());
     }
 }
@@ -270,8 +273,8 @@ var clearHistory = false;
 var deleteId = null;
 
 function deleteHistory() {
-    checkIfAuth(function () {
-        deleteServerHistory(deleteId, function (err, result) {
+    checkIfAuth(() => {
+        deleteServerHistory(deleteId, (err, result) => {
             if (!err) {
                 if (clearHistory) {
                     historyList.clear();
@@ -285,7 +288,7 @@ function deleteHistory() {
             deleteId = null;
             clearHistory = false;
         });
-    }, function () {
+    }, () => {
         if (clearHistory) {
             saveHistory([]);
             historyList.clear();
@@ -293,8 +296,8 @@ function deleteHistory() {
             deleteId = null;
         } else {
             if (!deleteId) return;
-            getHistory(function (notehistory) {
-                var newnotehistory = removeHistory(deleteId, notehistory);
+            getHistory(notehistory => {
+                const newnotehistory = removeHistory(deleteId, notehistory);
                 saveHistory(newnotehistory);
                 historyList.remove('id', deleteId);
                 checkHistoryList();
@@ -306,36 +309,36 @@ function deleteHistory() {
     });
 }
 
-$(".ui-delete-modal-confirm").click(function () {
+$(".ui-delete-modal-confirm").click(() => {
     deleteHistory();
 });
 
-$(".ui-import-from-browser").click(function () {
-    saveStorageHistoryToServer(function () {
+$(".ui-import-from-browser").click(() => {
+    saveStorageHistoryToServer(() => {
         parseStorageToHistory(historyList, parseHistoryCallback);
     });
 });
 
-$(".ui-save-history").click(function () {
-    getHistory(function (data) {
-        var history = JSON.stringify(data);
-        var blob = new Blob([history], {
+$(".ui-save-history").click(() => {
+    getHistory(data => {
+        const history = JSON.stringify(data);
+        const blob = new Blob([history], {
             type: "application/json;charset=utf-8"
         });
-        saveAs(blob, 'hackmd_history_' + moment().format('YYYYMMDDHHmmss'));
+        saveAs(blob, `hackmd_history_${moment().format('YYYYMMDDHHmmss')}`, true);
     });
 });
 
-$(".ui-open-history").bind("change", function (e) {
-    var files = e.target.files || e.dataTransfer.files;
-    var file = files[0];
-    var reader = new FileReader();
-    reader.onload = function () {
-        var notehistory = JSON.parse(reader.result);
+$(".ui-open-history").bind("change", e => {
+    const files = e.target.files || e.dataTransfer.files;
+    const file = files[0];
+    const reader = new FileReader();
+    reader.onload = () => {
+        const notehistory = JSON.parse(reader.result);
         //console.log(notehistory);
         if (!reader.result) return;
-        getHistory(function (data) {
-            var mergedata = data.concat(notehistory);
+        getHistory(data => {
+            let mergedata = data.concat(notehistory);
             mergedata = clearDuplicatedHistory(mergedata);
             saveHistory(mergedata);
             parseHistory(historyList, parseHistoryCallback);
@@ -345,18 +348,18 @@ $(".ui-open-history").bind("change", function (e) {
     reader.readAsText(file);
 });
 
-$(".ui-clear-history").click(function () {
+$(".ui-clear-history").click(() => {
     $('.ui-delete-modal-msg').text('Do you really want to clear all history?');
     $('.ui-delete-modal-item').html('There is no turning back.');
     clearHistory = true;
     deleteId = null;
 });
 
-$(".ui-refresh-history").click(function () {
-    var lastTags = $(".ui-use-tags").select2('val');
+$(".ui-refresh-history").click(() => {
+    const lastTags = $(".ui-use-tags").select2('val');
     $(".ui-use-tags").select2('val', '');
     historyList.filter();
-    var lastKeyword = $('.search').val();
+    const lastKeyword = $('.search').val();
     $('.search').val('');
     historyList.search();
     $('#history-list').slideUp('fast');
@@ -364,7 +367,7 @@ $(".ui-refresh-history").click(function () {
 
     resetCheckAuth();
     historyList.clear();
-    parseHistory(historyList, function (list, notehistory) {
+    parseHistory(historyList, (list, notehistory) => {
         parseHistoryCallback(list, notehistory);
         $(".ui-use-tags").select2('val', lastTags);
         $(".ui-use-tags").trigger('change');
@@ -375,16 +378,16 @@ $(".ui-refresh-history").click(function () {
     });
 });
 
-$(".ui-logout").click(function () {
+$(".ui-logout").click(() => {
     clearLoginState();
-    location.href = serverurl + '/logout';
+    location.href = `${serverurl}/logout`;
 });
 
-var filtertags = [];
+let filtertags = [];
 $(".ui-use-tags").select2({
     placeholder: $(".ui-use-tags").attr('placeholder'),
     multiple: true,
-    data: function () {
+    data() {
         return {
             results: filtertags
         };
@@ -394,7 +397,7 @@ $('.select2-input').css('width', 'inherit');
 buildTagsFilter([]);
 
 function buildTagsFilter(tags) {
-    for (var i = 0; i < tags.length; i++)
+    for (let i = 0; i < tags.length; i++)
         tags[i] = {
             id: i,
             text: S(tags[i]).unescapeHTML().s
@@ -402,17 +405,17 @@ function buildTagsFilter(tags) {
     filtertags = tags;
 }
 $(".ui-use-tags").on('change', function () {
-    var tags = [];
-    var data = $(this).select2('data');
-    for (var i = 0; i < data.length; i++)
+    const tags = [];
+    const data = $(this).select2('data');
+    for (let i = 0; i < data.length; i++)
         tags.push(data[i].text);
     if (tags.length > 0) {
-        historyList.filter(function (item) {
-            var values = item.values();
+        historyList.filter(item => {
+            const values = item.values();
             if (!values.tags) return false;
-            var found = false;
-            for (var i = 0; i < tags.length; i++) {
-                if (values.tags.indexOf(tags[i]) != -1) {
+            let found = false;
+            for (let i = 0; i < tags.length; i++) {
+                if (values.tags.includes(tags[i])) {
                     found = true;
                     break;
                 }
@@ -425,6 +428,6 @@ $(".ui-use-tags").on('change', function () {
     checkHistoryList();
 });
 
-$('.search').keyup(function () {
+$('.search').keyup(() => {
     checkHistoryList();
 });
diff --git a/public/js/extra.js b/public/js/extra.js
index 4a9cc76..a3e840d 100644
--- a/public/js/extra.js
+++ b/public/js/extra.js
@@ -1,15 +1,17 @@
 require('prismjs/themes/prism.css');
-
-var Prism = require('prismjs');
 require('prismjs/components/prism-wiki');
 require('prismjs/components/prism-haskell');
 require('prismjs/components/prism-go');
 require('prismjs/components/prism-typescript');
 require('prismjs/components/prism-jsx');
-var hljs = require('highlight.js');
-var PDFObject = require('pdfobject');
-var S = require('string');
-var saveAs = require('file-saver').saveAs;
+
+import Prism from 'prismjs';
+import hljs from 'highlight.js';
+import PDFObject from 'pdfobject';
+import S from 'string';
+import { saveAs } from 'file-saver';
+
+require('./lib/common/login');
 require('../vendor/md-toc');
 var Viz = require("viz.js");
 
@@ -22,9 +24,10 @@ window.lastchangeui = {
     user: $(".ui-lastchangeuser"),
     nouser: $(".ui-no-lastchangeuser")
 }
-var ownerui = $(".ui-owner");
 
-function updateLastChange() {
+const ownerui = $(".ui-owner");
+
+export function updateLastChange() {
     if (!lastchangeui) return;
     if (createtime) {
         if (createtime && !lastchangetime) {
@@ -32,7 +35,7 @@ function updateLastChange() {
         } else {
             lastchangeui.status.text('changed');
         }
-        var time = lastchangetime || createtime;
+        const time = lastchangetime || createtime;
         lastchangeui.time.html(moment(time).fromNow());
         lastchangeui.time.attr('title', moment(time).format('llll'));
     }
@@ -41,13 +44,14 @@ setInterval(updateLastChange, 60000);
 
 window.lastchangeuser = null;
 window.lastchangeuserprofile = null;
-function updateLastChangeUser() {
+
+export function updateLastChangeUser() {
     if (lastchangeui) {
       if (lastchangeuser && lastchangeuserprofile) {
-          var icon = lastchangeui.user.children('i');
+          const icon = lastchangeui.user.children('i');
           icon.attr('title', lastchangeuserprofile.name).tooltip('fixTitle');
           if (lastchangeuserprofile.photo)
-              icon.attr('style', 'background-image:url(' + lastchangeuserprofile.photo + ')');
+              icon.attr('style', `background-image:url(${lastchangeuserprofile.photo})`);
           lastchangeui.user.show();
           lastchangeui.nouser.hide();
       } else {
@@ -59,12 +63,13 @@ function updateLastChangeUser() {
 
 window.owner = null;
 window.ownerprofile = null;
-function updateOwner() {
+
+export function updateOwner() {
     if (ownerui) {
       if (owner && ownerprofile && owner !== lastchangeuser) {
-          var icon = ownerui.children('i');
+          const icon = ownerui.children('i');
           icon.attr('title', ownerprofile.name).tooltip('fixTitle');
-          var styleString = 'background-image:url(' + ownerprofile.photo + ')';
+          const styleString = `background-image:url(${ownerprofile.photo})`;
           if (ownerprofile.photo && icon.attr('style') !== styleString)
               icon.attr('style', styleString);
           ownerui.show();
@@ -76,11 +81,11 @@ function updateOwner() {
 
 //get title
 function getTitle(view) {
-    var title = "";
+    let title = "";
     if (md && md.meta && md.meta.title && (typeof md.meta.title == "string" || typeof md.meta.title == "number")) {
         title = md.meta.title;
     } else {
-        var h1s = view.find("h1");
+        const h1s = view.find("h1");
         if (h1s.length > 0) {
             title = h1s.first().text();
         } else {
@@ -91,8 +96,8 @@ function getTitle(view) {
 }
 
 //render title
-function renderTitle(view) {
-    var title = getTitle(view);
+export function renderTitle(view) {
+    let title = getTitle(view);
     if (title) {
         title += ' - HackMD';
     } else {
@@ -102,8 +107,8 @@ function renderTitle(view) {
 }
 
 //render filename
-function renderFilename(view) {
-    var filename = getTitle(view);
+export function renderFilename(view) {
+    let filename = getTitle(view);
     if (!filename) {
         filename = 'Untitled';
     }
@@ -111,29 +116,29 @@ function renderFilename(view) {
 }
 
 // render tags
-function renderTags(view) {
-    var tags = [];
-    var rawtags = [];
+export function renderTags(view) {
+    const tags = [];
+    const rawtags = [];
     if (md && md.meta && md.meta.tags && (typeof md.meta.tags == "string" || typeof md.meta.tags == "number")) {
-        var metaTags = ('' + md.meta.tags).split(',');
+        const metaTags = (`${md.meta.tags}`).split(',');
         for (var i = 0; i < metaTags.length; i++) {
-            var text = metaTags[i].trim();
+            const text = metaTags[i].trim();
             if (text) rawtags.push(text);
         }
     } else {
-        view.find('h6').each(function (key, value) {
+        view.find('h6').each((key, value) => {
             if (/^tags/gmi.test($(value).text())) {
-                var codes = $(value).find("code");
-                for (var i = 0; i < codes.length; i++) {
-                    var text = codes[i].innerHTML.trim();
+                const codes = $(value).find("code");
+                for (let i = 0; i < codes.length; i++) {
+                    const text = codes[i].innerHTML.trim();
                     if (text) rawtags.push(text);
                 }
             }
         });
     }
     for (var i = 0; i < rawtags.length; i++) {
-        var found = false;
-        for (var j = 0; j < tags.length; j++) {
+        let found = false;
+        for (let j = 0; j < tags.length; j++) {
             if (tags[j] == rawtags[i]) {
                 found = true;
                 break;
@@ -146,13 +151,13 @@ function renderTags(view) {
 }
 
 function slugifyWithUTF8(text) {
-    var newText = S(text.toLowerCase()).trim().stripTags().dasherize().s;
+    let newText = S(text.toLowerCase()).trim().stripTags().dasherize().s;
     newText = newText.replace(/([\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\[\\\]\^\`\{\|\}\~])/g, '');
     return newText;
 }
 
-function isValidURL(str) {
-    var pattern = new RegExp('^(https?:\\/\\/)?' + // protocol
+export function isValidURL(str) {
+    const pattern = new RegExp('^(https?:\\/\\/)?' + // protocol
         '((([a-z\\d]([a-z\\d-]*[a-z\\d])*)\\.)+[a-z]{2,}|' + // domain name
         '((\\d{1,3}\\.){3}\\d{1,3}))' + // OR ip (v4) address
         '(\\:\\d+)?(\\/[-a-z\\d%_.~+]*)*' + // port and path
@@ -166,12 +171,12 @@ function isValidURL(str) {
 }
 
 //parse meta
-function parseMeta(md, edit, view, toc, tocAffix) {
-    var lang = null;
-    var dir = null;
-    var breaks = true;
+export function parseMeta(md, edit, view, toc, tocAffix) {
+    let lang = null;
+    let dir = null;
+    let breaks = true;
     if (md && md.meta) {
-        var meta = md.meta;
+        const meta = md.meta;
         lang = meta.lang;
         dir = meta.dir;
         breaks = meta.breaks;
@@ -211,13 +216,13 @@ function parseMeta(md, edit, view, toc, tocAffix) {
 window.viewAjaxCallback = null;
 
 //regex for extra tags
-var spaceregex = /\s*/;
-var notinhtmltagregex = /(?![^<]*>|[^<>]*<\/)/;
-var coloregex = /\[color=([#|\(|\)|\s|\,|\w]*?)\]/;
+const spaceregex = /\s*/;
+const notinhtmltagregex = /(?![^<]*>|[^<>]*<\/)/;
+let coloregex = /\[color=([#|\(|\)|\s|\,|\w]*?)\]/;
 coloregex = new RegExp(coloregex.source + notinhtmltagregex.source, "g");
-var nameregex = /\[name=(.*?)\]/;
-var timeregex = /\[time=([:|,|+|-|\(|\)|\s|\w]*?)\]/;
-var nameandtimeregex = new RegExp(nameregex.source + spaceregex.source + timeregex.source + notinhtmltagregex.source, "g");
+let nameregex = /\[name=(.*?)\]/;
+let timeregex = /\[time=([:|,|+|-|\(|\)|\s|\w]*?)\]/;
+const nameandtimeregex = new RegExp(nameregex.source + spaceregex.source + timeregex.source + notinhtmltagregex.source, "g");
 nameregex = new RegExp(nameregex.source + notinhtmltagregex.source, "g");
 timeregex = new RegExp(timeregex.source + notinhtmltagregex.source, "g");
 
@@ -232,35 +237,36 @@ function replaceExtraTags(html) {
 if (typeof mermaid !== 'undefined' && mermaid) mermaid.startOnLoad = false;
 
 //dynamic event or object binding here
-function finishView(view) {
+export function finishView(view) {
     //todo list
-    var lis = view.find('li.raw').removeClass("raw").sortByDepth().toArray();
-    for (var i = 0; i < lis.length; i++) {
-        var li = lis[i];
-        var html = $(li).clone()[0].innerHTML;
-        var p = $(li).children('p');
+    const lis = view.find('li.raw').removeClass("raw").sortByDepth().toArray();
+
+    for (let li of lis) {
+        let html = $(li).clone()[0].innerHTML;
+        const p = $(li).children('p');
         if (p.length == 1) {
             html = p.html();
             li = p[0];
         }
         html = replaceExtraTags(html);
         li.innerHTML = html;
-        var disabled = 'disabled';
+        let disabled = 'disabled';
         if(typeof editor !== 'undefined' && havePermission())
             disabled = '';
         if (/^\s*\[[x ]\]\s*/.test(html)) {
-            li.innerHTML = html.replace(/^\s*\[ \]\s*/, '<input type="checkbox" class="task-list-item-checkbox "' + disabled + '><label></label>')
-                .replace(/^\s*\[x\]\s*/, '<input type="checkbox" class="task-list-item-checkbox" checked ' + disabled + '><label></label>');
-            lis[i].setAttribute('class', 'task-list-item');
+            li.innerHTML = html.replace(/^\s*\[ \]\s*/, `<input type="checkbox" class="task-list-item-checkbox "${disabled}><label></label>`)
+                .replace(/^\s*\[x\]\s*/, `<input type="checkbox" class="task-list-item-checkbox" checked ${disabled}><label></label>`);
+            li.setAttribute('class', 'task-list-item');
         }
         if (typeof editor !== 'undefined' && havePermission())
             $(li).find('input').change(toggleTodoEvent);
         //color tag in list will convert it to tag icon with color
-        var tag_color = $(li).closest('ul').find(".color");
-        tag_color.each(function (key, value) {
+        const tag_color = $(li).closest('ul').find(".color");
+        tag_color.each((key, value) => {
             $(value).addClass('fa fa-tag').css('color', $(value).attr('data-color'));
         });
     }
+
     //youtube
     view.find("div.youtube.raw").removeClass("raw")
         .click(function () {
@@ -271,41 +277,41 @@ function finishView(view) {
         .click(function () {
             imgPlayiframe(this, '//player.vimeo.com/video/');
         })
-        .each(function (key, value) {
+        .each((key, value) => {
             $.ajax({
                 type: 'GET',
-                url: '//vimeo.com/api/v2/video/' + $(value).attr('data-videoid') + '.json',
+                url: `//vimeo.com/api/v2/video/${$(value).attr('data-videoid')}.json`,
                 jsonp: 'callback',
                 dataType: 'jsonp',
-                success: function (data) {
-                    var thumbnail_src = data[0].thumbnail_large;
-                    var image = '<img src="' + thumbnail_src + '" />';
+                success(data) {
+                    const thumbnail_src = data[0].thumbnail_large;
+                    const image = `<img src="${thumbnail_src}" />`;
                     $(value).prepend(image);
                     if(viewAjaxCallback) viewAjaxCallback();
                 }
             });
         });
     //gist
-    view.find("code[data-gist-id]").each(function (key, value) {
+    view.find("code[data-gist-id]").each((key, value) => {
         if ($(value).children().length == 0)
             $(value).gist(viewAjaxCallback);
     });
     //sequence diagram
-    var sequences = view.find("div.sequence-diagram.raw").removeClass("raw");
-    sequences.each(function (key, value) {
+    const sequences = view.find("div.sequence-diagram.raw").removeClass("raw");
+    sequences.each((key, value) => {
         try {
             var $value = $(value);
-            var $ele = $(value).parent().parent();
+            const $ele = $(value).parent().parent();
 
-            var sequence = $value;
+            const sequence = $value;
             sequence.sequenceDiagram({
                 theme: 'simple'
             });
 
             $ele.addClass('sequence-diagram');
             $value.children().unwrap().unwrap();
-            var svg = $ele.find('> svg');
-            svg[0].setAttribute('viewBox', '0 0 ' + svg.attr('width') + ' ' + svg.attr('height'));
+            const svg = $ele.find('> svg');
+            svg[0].setAttribute('viewBox', `0 0 ${svg.attr('width')} ${svg.attr('height')}`);
             svg[0].setAttribute('preserveAspectRatio', 'xMidYMid meet');
         } catch (err) {
             $value.unwrap();
@@ -314,13 +320,13 @@ function finishView(view) {
         }
     });
     //flowchart
-    var flow = view.find("div.flow-chart.raw").removeClass("raw");
-    flow.each(function (key, value) {
+    const flow = view.find("div.flow-chart.raw").removeClass("raw");
+    flow.each((key, value) => {
         try {
             var $value = $(value);
-            var $ele = $(value).parent().parent();
+            const $ele = $(value).parent().parent();
 
-            var chart = flowchart.parse($value.text());
+            const chart = flowchart.parse($value.text());
             $value.html('');
             chart.drawSVG(value, {
                 'line-width': 2,
@@ -339,41 +345,32 @@ function finishView(view) {
     });
     //graphviz
     var graphvizs = view.find("div.graphviz.raw").removeClass("raw");
-    function parseGraphviz(key, value) {
-        var $value = $(value);
-        var $ele = $(value).parent().parent();
-
-        var graphviz = Viz($value.text());
-        if (!graphviz) throw Error('viz.js output empty graph');
-        $value.html(graphviz);
-
-        $ele.addClass('graphviz');
-        $value.children().unwrap().unwrap();
-    }
     graphvizs.each(function (key, value) {
         try {
-            parseGraphviz(key, value);
+            var $value = $(value);
+            var $ele = $(value).parent().parent();
+
+            var graphviz = Viz($value.text());
+            if (!graphviz) throw Error('viz.js output empty graph');
+            $value.html(graphviz);
+
+            $ele.addClass('graphviz');
+            $value.children().unwrap().unwrap();
         } catch (err) {
-            // workaround for graphviz not recover from error
-            try {
-                parseGraphviz(key, value);
-            } catch (err) {
-                var $value = $(value);
-                $value.unwrap();
-                $value.parent().append('<div class="alert alert-warning">' + err + '</div>');
-                console.warn(err);
-            }
+            $value.unwrap();
+            $value.parent().append('<div class="alert alert-warning">' + err + '</div>');
+            console.warn(err);
         }
     });
     //mermaid
-    var mermaids = view.find("div.mermaid.raw").removeClass("raw");
-    mermaids.each(function (key, value) {
+    const mermaids = view.find("div.mermaid.raw").removeClass("raw");
+    mermaids.each((key, value) => {
         try {
             var $value = $(value);
-            var $ele = $(value).closest('pre');
+            const $ele = $(value).closest('pre');
 
-            var mermaidError = null;
-            mermaid.parseError = function (err, hash) {
+            let mermaidError = null;
+            mermaid.parseError = (err, hash) => {
                 mermaidError = err;
             };
 
@@ -391,44 +388,44 @@ function finishView(view) {
         }
     });
     //image href new window(emoji not included)
-    var images = view.find("img.raw[src]").removeClass("raw");
-    images.each(function (key, value) {
+    const images = view.find("img.raw[src]").removeClass("raw");
+    images.each((key, value) => {
         // if it's already wrapped by link, then ignore
-        var $value = $(value);
-        $value[0].onload = function (e) {
+        const $value = $(value);
+        $value[0].onload = e => {
             if(viewAjaxCallback) viewAjaxCallback();
         };
     });
     //blockquote
-    var blockquote = view.find("blockquote.raw").removeClass("raw");
-    var blockquote_p = blockquote.find("p");
-    blockquote_p.each(function (key, value) {
-        var html = $(value).html();
+    const blockquote = view.find("blockquote.raw").removeClass("raw");
+    const blockquote_p = blockquote.find("p");
+    blockquote_p.each((key, value) => {
+        let html = $(value).html();
         html = replaceExtraTags(html);
         $(value).html(html);
     });
     //color tag in blockquote will change its left border color
-    var blockquote_color = blockquote.find(".color");
-    blockquote_color.each(function (key, value) {
+    const blockquote_color = blockquote.find(".color");
+    blockquote_color.each((key, value) => {
         $(value).closest("blockquote").css('border-left-color', $(value).attr('data-color'));
     });
     //slideshare
     view.find("div.slideshare.raw").removeClass("raw")
-        .each(function (key, value) {
+        .each((key, value) => {
             $.ajax({
                 type: 'GET',
-                url: '//www.slideshare.net/api/oembed/2?url=http://www.slideshare.net/' + $(value).attr('data-slideshareid') + '&format=json',
+                url: `//www.slideshare.net/api/oembed/2?url=http://www.slideshare.net/${$(value).attr('data-slideshareid')}&format=json`,
                 jsonp: 'callback',
                 dataType: 'jsonp',
-                success: function (data) {
-                    var $html = $(data.html);
-                    var iframe = $html.closest('iframe');
-                    var caption = $html.closest('div');
-                    var inner = $('<div class="inner"></div>').append(iframe);
-                    var height = iframe.attr('height');
-                    var width = iframe.attr('width');
-                    var ratio = (height / width) * 100;
-                    inner.css('padding-bottom', ratio + '%');
+                success(data) {
+                    const $html = $(data.html);
+                    const iframe = $html.closest('iframe');
+                    const caption = $html.closest('div');
+                    const inner = $('<div class="inner"></div>').append(iframe);
+                    const height = iframe.attr('height');
+                    const width = iframe.attr('width');
+                    const ratio = (height / width) * 100;
+                    inner.css('padding-bottom', `${ratio}%`);
                     $(value).html(inner).append(caption);
                     if(viewAjaxCallback) viewAjaxCallback();
                 }
@@ -436,31 +433,31 @@ function finishView(view) {
         });
     //speakerdeck
     view.find("div.speakerdeck.raw").removeClass("raw")
-        .each(function (key, value) {
-            var url = 'https://speakerdeck.com/oembed.json?url=https%3A%2F%2Fspeakerdeck.com%2F' + encodeURIComponent($(value).attr('data-speakerdeckid'));
+        .each((key, value) => {
+            const url = `https://speakerdeck.com/oembed.json?url=https%3A%2F%2Fspeakerdeck.com%2F${encodeURIComponent($(value).attr('data-speakerdeckid'))}`;
             //use yql because speakerdeck not support jsonp
             $.ajax({
                 url: 'https://query.yahooapis.com/v1/public/yql',
                 data: {
-                    q: "select * from json where url ='" + url + "'",
+                    q: `select * from json where url ='${url}'`,
                     format: "json"
                 },
                 dataType: "jsonp",
-                success: function (data) {
+                success(data) {
                     if (!data.query || !data.query.results) return;
-                    var json = data.query.results.json;
-                    var html = json.html;
+                    const json = data.query.results.json;
+                    const html = json.html;
                     var ratio = json.height / json.width;
                     $(value).html(html);
-                    var iframe = $(value).children('iframe');
-                    var src = iframe.attr('src');
+                    const iframe = $(value).children('iframe');
+                    const src = iframe.attr('src');
                     if (src.indexOf('//') == 0)
-                        iframe.attr('src', 'https:' + src);
-                    var inner = $('<div class="inner"></div>').append(iframe);
-                    var height = iframe.attr('height');
-                    var width = iframe.attr('width');
+                        iframe.attr('src', `https:${src}`);
+                    const inner = $('<div class="inner"></div>').append(iframe);
+                    const height = iframe.attr('height');
+                    const width = iframe.attr('width');
                     var ratio = (height / width) * 100;
-                    inner.css('padding-bottom', ratio + '%');
+                    inner.css('padding-bottom', `${ratio}%`);
                     $(value).html(inner);
                     if(viewAjaxCallback) viewAjaxCallback();
                 }
@@ -469,8 +466,8 @@ function finishView(view) {
     //pdf
     view.find("div.pdf.raw").removeClass("raw")
             .each(function (key, value) {
-                var url = $(value).attr('data-pdfurl');
-                var inner = $('<div></div>');
+                const url = $(value).attr('data-pdfurl');
+                const inner = $('<div></div>');
                 $(this).append(inner);
                 PDFObject.embed(url, inner, {
                     height: '400px'
@@ -478,12 +475,12 @@ function finishView(view) {
             });
     //syntax highlighting
     view.find("code.raw").removeClass("raw")
-        .each(function (key, value) {
-            var langDiv = $(value);
+        .each((key, value) => {
+            const langDiv = $(value);
             if (langDiv.length > 0) {
-                var reallang = langDiv[0].className.replace(/hljs|wrap/g, '').trim();
-                var codeDiv = langDiv.find('.code');
-                var code = "";
+                const reallang = langDiv[0].className.replace(/hljs|wrap/g, '').trim();
+                const codeDiv = langDiv.find('.code');
+                let code = "";
                 if (codeDiv.length > 0) code = codeDiv.html();
                 else code = langDiv.html();
                 if (!reallang) {
@@ -502,8 +499,8 @@ function finishView(view) {
                     };
                 } else {
                     code = S(code).unescapeHTML().s;
-                    var languages = hljs.listLanguages();
-                    if (languages.indexOf(reallang) == -1) {
+                    const languages = hljs.listLanguages();
+                    if (!languages.includes(reallang)) {
                         var result = hljs.highlightAuto(code);
                     } else {
                         var result = hljs.highlight(reallang, code);
@@ -514,7 +511,7 @@ function finishView(view) {
             }
         });
     //mathjax
-    var mathjaxdivs = view.find('span.mathjax.raw').removeClass("raw").toArray();
+    const mathjaxdivs = view.find('span.mathjax.raw').removeClass("raw").toArray();
     try {
         if (mathjaxdivs.length > 1) {
             MathJax.Hub.Queue(["Typeset", MathJax.Hub, mathjaxdivs]);
@@ -531,16 +528,16 @@ function finishView(view) {
 }
 
 //only static transform should be here
-function postProcess(code) {
-    var result = $('<div>' + code + '</div>');
+export function postProcess(code) {
+    const result = $(`<div>${code}</div>`);
     //link should open in new window or tab
     result.find('a:not([href^="#"]):not([target])').attr('target', '_blank');
 	//update continue line numbers
-	var linenumberdivs = result.find('.gutter.linenumber').toArray();
-	for (var i = 0; i < linenumberdivs.length; i++) {
+	const linenumberdivs = result.find('.gutter.linenumber').toArray();
+	for (let i = 0; i < linenumberdivs.length; i++) {
 		if ($(linenumberdivs[i]).hasClass('continue')) {
-			var startnumber = linenumberdivs[i - 1] ? parseInt($(linenumberdivs[i - 1]).find('> span').last().attr('data-linenumber')) : 0;
-			$(linenumberdivs[i]).find('> span').each(function(key, value) {
+			const startnumber = linenumberdivs[i - 1] ? parseInt($(linenumberdivs[i - 1]).find('> span').last().attr('data-linenumber')) : 0;
+			$(linenumberdivs[i]).find('> span').each((key, value) => {
 				$(value).attr('data-linenumber', startnumber + key + 1);
 			});
 		}
@@ -560,8 +557,8 @@ function postProcess(code) {
 window.postProcess = postProcess;
 
 function generateCleanHTML(view) {
-    var src = view.clone();
-    var eles = src.find('*');
+    const src = view.clone();
+    const eles = src.find('*');
     //remove syncscroll parts
     eles.removeClass('part');
     src.find('*[class=""]').removeAttr('class');
@@ -572,24 +569,24 @@ function generateCleanHTML(view) {
     //disable todo list
     src.find("input.task-list-item-checkbox").attr('disabled', '');
     //replace emoji image path
-    src.find("img.emoji").each(function (key, value) {
-        var name = $(value).attr('alt');
+    src.find("img.emoji").each((key, value) => {
+        let name = $(value).attr('alt');
         name = name.substr(1);
         name = name.slice(0, name.length - 1);
-        $(value).attr('src', 'https://www.tortue.me/emoji/' + name + '.png');
+        $(value).attr('src', `https://www.tortue.me/emoji/${name}.png`);
     });
     //replace video to iframe
-    src.find("div[data-videoid]").each(function (key, value) {
-        var id = $(value).attr('data-videoid');
-        var style = $(value).attr('style');
-        var url = null;
+    src.find("div[data-videoid]").each((key, value) => {
+        const id = $(value).attr('data-videoid');
+        const style = $(value).attr('style');
+        let url = null;
         if ($(value).hasClass('youtube')) {
             url = 'https://www.youtube.com/embed/';
         } else if ($(value).hasClass('vimeo')) {
             url = 'https://player.vimeo.com/video/';
         }
         if (url) {
-            var iframe = $('<iframe frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>');
+            const iframe = $('<iframe frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>');
             iframe.attr('src', url + id);
             iframe.attr('style', style);
             $(value).html(iframe);
@@ -598,65 +595,63 @@ function generateCleanHTML(view) {
     return src;
 }
 
-function exportToRawHTML(view) {
-    var filename = renderFilename(ui.area.markdown) + '.html';
-    var src = generateCleanHTML(view);
+export function exportToRawHTML(view) {
+    const filename = `${renderFilename(ui.area.markdown)}.html`;
+    const src = generateCleanHTML(view);
     $(src).find('a.anchor').remove();
-    var html = src[0].outerHTML;
-    var blob = new Blob([html], {
+    const html = src[0].outerHTML;
+    const blob = new Blob([html], {
         type: "text/html;charset=utf-8"
     });
-    saveAs(blob, filename);
+    saveAs(blob, filename, true);
 }
 
-var common = require('./common.js');
 //extract markdown body to html and compile to template
-function exportToHTML(view) {
-    var title = renderTitle(ui.area.markdown);
-    var filename = renderFilename(ui.area.markdown) + '.html';
-    var src = generateCleanHTML(view);
+export function exportToHTML(view) {
+    const title = renderTitle(ui.area.markdown);
+    const filename = `${renderFilename(ui.area.markdown)}.html`;
+    const src = generateCleanHTML(view);
     //generate toc
-    var toc = $('#ui-toc').clone();
+    const toc = $('#ui-toc').clone();
     toc.find('*').removeClass('active').find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll');
-    var tocAffix = $('#ui-toc-affix').clone();
+    const tocAffix = $('#ui-toc-affix').clone();
     tocAffix.find('*').removeClass('active').find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll');
     //generate html via template
-    $.get(serverurl + '/build/html.min.css', function (css) {
-        $.get(serverurl + '/views/html.hbs', function (data) {
-            var template = Handlebars.compile(data);
-            var context = {
+    $.get(`${serverurl}/build/html.min.css`, css => {
+        $.get(`${serverurl}/views/html.hbs`, data => {
+            const template = Handlebars.compile(data);
+            const context = {
                 url: serverurl,
-                title: title,
-                css: css,
+                title,
+                css,
                 html: src[0].outerHTML,
                 'ui-toc': toc.html(),
                 'ui-toc-affix': tocAffix.html(),
-                lang: (md && md.meta && md.meta.lang) ? 'lang="' + md.meta.lang + '"' : null,
-                dir: (md && md.meta && md.meta.dir) ? 'dir="' + md.meta.dir + '"' : null
+                lang: (md && md.meta && md.meta.lang) ? `lang="${md.meta.lang}"` : null,
+                dir: (md && md.meta && md.meta.dir) ? `dir="${md.meta.dir}"` : null
             };
-            var html = template(context);
+            const html = template(context);
             //        console.log(html);
-            var blob = new Blob([html], {
+            const blob = new Blob([html], {
                 type: "text/html;charset=utf-8"
             });
-            saveAs(blob, filename);
+            saveAs(blob, filename, true);
         });
     });
 }
 
 //jQuery sortByDepth
 $.fn.sortByDepth = function () {
-    var ar = this.map(function () {
+    const ar = this.map(function () {
             return {
                 length: $(this).parents().length,
                 elt: this
             }
-        }).get(),
-        result = [],
-        i = ar.length;
-    ar.sort(function (a, b) {
-        return a.length - b.length;
-    });
+        }).get();
+
+    const result = [];
+    let i = ar.length;
+    ar.sort((a, b) => a.length - b.length);
     while (i--) {
         result.push(ar[i].elt);
     }
@@ -664,16 +659,16 @@ $.fn.sortByDepth = function () {
 };
 
 function toggleTodoEvent(e) {
-    var startline = $(this).closest('li').attr('data-startline') - 1;
-    var line = editor.getLine(startline);
-    var matches = line.match(/^[>\s]*[\-\+\*]\s\[([x ])\]/);
+    const startline = $(this).closest('li').attr('data-startline') - 1;
+    const line = editor.getLine(startline);
+    const matches = line.match(/^[>\s]*[\-\+\*]\s\[([x ])\]/);
     if (matches && matches.length >= 2) {
-        var checked = null;
+        let checked = null;
         if (matches[1] == 'x')
             checked = true;
         else if (matches[1] == ' ')
             checked = false;
-        var replacements = matches[0].match(/(^[>\s]*[\-\+\*]\s\[)([x ])(\])/);
+        const replacements = matches[0].match(/(^[>\s]*[\-\+\*]\s\[)([x ])(\])/);
         editor.replaceRange(checked ? ' ' : 'x', {
             line: startline,
             ch: replacements[1].length
@@ -689,11 +684,11 @@ function removeHash() {
     history.pushState("", document.title, window.location.pathname + window.location.search);
 }
 
-var tocExpand = false;
+let tocExpand = false;
 
 function checkExpandToggle() {
-    var toc = $('.ui-toc-dropdown .toc');
-    var toggle = $('.expand-toggle');
+    const toc = $('.ui-toc-dropdown .toc');
+    const toggle = $('.expand-toggle');
     if (!tocExpand) {
         toc.removeClass('expand');
         toggle.text('Expand all');
@@ -704,8 +699,8 @@ function checkExpandToggle() {
 }
 
 //toc
-function generateToc(id) {
-    var target = $('#' + id);
+export function generateToc(id) {
+    const target = $(`#${id}`);
     target.html('');
     new Toc('doc', {
         'level': 3,
@@ -717,25 +712,25 @@ function generateToc(id) {
     });
     if (target.text() == 'undefined')
         target.html('');
-    var tocMenu = $('<div class="toc-menu"></div');
-    var toggle = $('<a class="expand-toggle" href="#">Expand all</a>');
-    var backtotop = $('<a class="back-to-top" href="#">Back to top</a>');
-    var gotobottom = $('<a class="go-to-bottom" href="#">Go to bottom</a>');
+    const tocMenu = $('<div class="toc-menu"></div');
+    const toggle = $('<a class="expand-toggle" href="#">Expand all</a>');
+    const backtotop = $('<a class="back-to-top" href="#">Back to top</a>');
+    const gotobottom = $('<a class="go-to-bottom" href="#">Go to bottom</a>');
     checkExpandToggle();
-    toggle.click(function (e) {
+    toggle.click(e => {
         e.preventDefault();
         e.stopPropagation();
         tocExpand = !tocExpand;
         checkExpandToggle();
     });
-    backtotop.click(function (e) {
+    backtotop.click(e => {
         e.preventDefault();
         e.stopPropagation();
         if (scrollToTop)
             scrollToTop();
         removeHash();
     });
-    gotobottom.click(function (e) {
+    gotobottom.click(e => {
         e.preventDefault();
         e.stopPropagation();
         if (scrollToBottom)
@@ -747,18 +742,18 @@ function generateToc(id) {
 }
 
 //smooth all hash trigger scrolling
-function smoothHashScroll() {
-    var hashElements = $("a[href^='#']:not([smoothhashscroll])").toArray();
-    for (var i = 0; i < hashElements.length; i++) {
-        var element = hashElements[i];
-        var $element = $(element);
-        var hash = element.hash;
+export function smoothHashScroll() {
+    const hashElements = $("a[href^='#']:not([smoothhashscroll])").toArray();
+
+    for (const element of hashElements) {
+        const $element = $(element);
+        const hash = element.hash;
         if (hash) {
             $element.on('click', function (e) {
                 // store hash
-                var hash = decodeURIComponent(this.hash);
+                const hash = decodeURIComponent(this.hash);
                 // escape special characters in jquery selector
-                var $hash = $(hash.replace(/(:|\.|\[|\]|,)/g, "\\$1"));
+                const $hash = $(hash.replace(/(:|\.|\[|\]|,)/g, "\\$1"));
                 // return if no element been selected
                 if ($hash.length <= 0) return;
                 // prevent default anchor click behavior
@@ -766,7 +761,7 @@ function smoothHashScroll() {
                 // animate
                 $('body, html').stop(true, true).animate({
                     scrollTop: $hash.offset().top
-                }, 100, "linear", function () {
+                }, 100, "linear", () => {
                     // when done, add hash to url
                     // (default click behaviour)
                     window.location.hash = hash;
@@ -779,29 +774,30 @@ function smoothHashScroll() {
 
 function imgPlayiframe(element, src) {
     if (!$(element).attr("data-videoid")) return;
-    var iframe = $("<iframe frameborder='0' webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>");
-    $(iframe).attr("src", src + $(element).attr("data-videoid") + '?autoplay=1');
+    const iframe = $("<iframe frameborder='0' webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>");
+    $(iframe).attr("src", `${src + $(element).attr("data-videoid")}?autoplay=1`);
     $(element).find('img').css('visibility', 'hidden');
     $(element).append(iframe);
 }
 
-var anchorForId = function (id) {
-    var anchor = document.createElement("a");
+const anchorForId = id => {
+    const anchor = document.createElement("a");
     anchor.className = "anchor hidden-xs";
-    anchor.href = "#" + id;
+    anchor.href = `#${id}`;
     anchor.innerHTML = "<span class=\"octicon octicon-link\"></span>";
     anchor.title = id;
     return anchor;
 };
 
-var linkifyAnchors = function (level, containingElement) {
-    var headers = containingElement.getElementsByTagName("h" + level);
-    for (var h = 0; h < headers.length; h++) {
-        var header = headers[h];
+const linkifyAnchors = (level, containingElement) => {
+    const headers = containingElement.getElementsByTagName(`h${level}`);
+
+    for (let i = 0, l = headers.length; i < l; i++) {
+        let header = headers[i];
         if (header.getElementsByClassName("anchor").length == 0) {
             if (typeof header.id == "undefined" || header.id == "") {
                 //to escape characters not allow in css and humanize
-                var id = slugifyWithUTF8(getHeaderContent(header));
+                const id = slugifyWithUTF8(getHeaderContent(header));
                 header.id = id;
             }
             header.insertBefore(anchorForId(header.id), header.firstChild);
@@ -809,49 +805,49 @@ var linkifyAnchors = function (level, containingElement) {
     }
 };
 
-function autoLinkify(view) {
-    var contentBlock = view[0];
+export function autoLinkify(view) {
+    const contentBlock = view[0];
     if (!contentBlock) {
         return;
     }
-    for (var level = 1; level <= 6; level++) {
+    for (let level = 1; level <= 6; level++) {
         linkifyAnchors(level, contentBlock);
     }
 }
 
 function getHeaderContent(header) {
-    var headerHTML = $(header).clone();
+    const headerHTML = $(header).clone();
     headerHTML.find('.MathJax_Preview').remove();
     headerHTML.find('.MathJax').remove();
     return headerHTML[0].innerHTML;
 }
 
-function deduplicatedHeaderId(view) {
-    var headers = view.find(':header.raw').removeClass('raw').toArray();
-    for (var i = 0; i < headers.length; i++) {
-        var id = $(headers[i]).attr('id');
+export function deduplicatedHeaderId(view) {
+    const headers = view.find(':header.raw').removeClass('raw').toArray();
+    for (let i = 0; i < headers.length; i++) {
+        const id = $(headers[i]).attr('id');
         if (!id) continue;
-        var duplicatedHeaders = view.find(':header[id="' + id + '"]').toArray();
-        for (var j = 0; j < duplicatedHeaders.length; j++) {
+        const duplicatedHeaders = view.find(`:header[id="${id}"]`).toArray();
+        for (let j = 0; j < duplicatedHeaders.length; j++) {
             if (duplicatedHeaders[j] != headers[i]) {
-                var newId = id + j;
-                var $duplicatedHeader = $(duplicatedHeaders[j]);
+                const newId = id + j;
+                const $duplicatedHeader = $(duplicatedHeaders[j]);
                 $duplicatedHeader.attr('id', newId);
-                var $headerLink = $duplicatedHeader.find('> .header-link');
-                $headerLink.attr('href', '#' + newId);
+                const $headerLink = $duplicatedHeader.find(`> a.anchor[href="#${id}"]`);
+                $headerLink.attr('href', `#${newId}`);
                 $headerLink.attr('title', newId);
             }
         }
     }
 }
 
-function renderTOC(view) {
-    var tocs = view.find('.toc').toArray();
-	for (var i = 0; i < tocs.length; i++) {
-        var toc = $(tocs[i]);
-        var id = 'toc' + i;
+export function renderTOC(view) {
+    const tocs = view.find('.toc').toArray();
+	for (let i = 0; i < tocs.length; i++) {
+        const toc = $(tocs[i]);
+        const id = `toc${i}`;
         toc.attr('id', id);
-        var target = $('#' + id);
+        const target = $(`#${id}`);
         target.html('');
         new Toc('doc', {
             'level': 3,
@@ -866,8 +862,8 @@ function renderTOC(view) {
     }
 }
 
-function scrollToHash() {
-    var hash = location.hash;
+export function scrollToHash() {
+    const hash = location.hash;
     location.hash = "";
     location.hash = hash;
 }
@@ -877,39 +873,39 @@ function highlightRender(code, lang) {
         return;
     code = S(code).escapeHTML().s
     if (lang == 'sequence') {
-        return '<div class="sequence-diagram raw">' + code + '</div>';
+        return `<div class="sequence-diagram raw">${code}</div>`;
     } else if (lang == 'flow') {
-        return '<div class="flow-chart raw">' + code + '</div>';
+        return `<div class="flow-chart raw">${code}</div>`;
     } else if (lang == 'graphviz') {
-        return '<div class="graphviz raw">' + code + '</div>';
+        return `<div class="graphviz raw">${code}</div>`;
     } else if (lang == 'mermaid') {
-        return '<div class="mermaid raw">' + code + '</div>';
+        return `<div class="mermaid raw">${code}</div>`;
     }
-    var result = {
+    const result = {
         value: code
     };
-	var showlinenumbers = /\=$|\=\d+$|\=\+$/.test(lang);
+	const showlinenumbers = /\=$|\=\d+$|\=\+$/.test(lang);
     if (showlinenumbers) {
-		var startnumber = 1;
-		var matches = lang.match(/\=(\d+)$/);
+		let startnumber = 1;
+		const matches = lang.match(/\=(\d+)$/);
 		if (matches)
 			startnumber = parseInt(matches[1]);
-        var lines = result.value.split('\n');
-        var linenumbers = [];
-        for (var i = 0; i < lines.length - 1; i++) {
-            linenumbers[i] = "<span data-linenumber='" + (startnumber + i) + "'></span>";
+        const lines = result.value.split('\n');
+        const linenumbers = [];
+        for (let i = 0; i < lines.length - 1; i++) {
+            linenumbers[i] = `<span data-linenumber='${startnumber + i}'></span>`;
         }
-		var continuelinenumber = /\=\+$/.test(lang);
-        var linegutter = "<div class='gutter linenumber" + (continuelinenumber ? " continue" : "") + "'>" + linenumbers.join('\n') + "</div>";
-        result.value = "<div class='wrapper'>" + linegutter + "<div class='code'>" + result.value + "</div></div>";
+		const continuelinenumber = /\=\+$/.test(lang);
+        const linegutter = `<div class='gutter linenumber${continuelinenumber ? " continue" : ""}'>${linenumbers.join('\n')}</div>`;
+        result.value = `<div class='wrapper'>${linegutter}<div class='code'>${result.value}</div></div>`;
     }
     return result.value;
 }
 
-var markdownit = require('markdown-it');
-var markdownitContainer = require('markdown-it-container');
+import markdownit from 'markdown-it';
+import markdownitContainer from 'markdown-it-container';
 
-var md = markdownit('default', {
+export let md = markdownit('default', {
     html: true,
     breaks: true,
     langPrefix: "",
@@ -945,19 +941,17 @@ emojify.setConfig({
         elements: ['script', 'textarea', 'a', 'pre', 'code', 'svg'],
         classes: ['no-emojify']
     },
-    img_dir: serverurl + '/build/emojify.js/dist/images/basic',
+    img_dir: `${serverurl}/build/emojify.js/dist/images/basic`,
     ignore_emoticons: true
 });
 
-md.renderer.rules.emoji = function(token, idx) {
-    return emojify.replace(':' + token[idx].markup + ':');
-};
+md.renderer.rules.emoji = (token, idx) => emojify.replace(`:${token[idx].markup}:`);
 
 function renderContainer(tokens, idx, options, env, self) {
     tokens[idx].attrJoin('role', 'alert');
     tokens[idx].attrJoin('class', 'alert');
-    tokens[idx].attrJoin('class', 'alert-' + tokens[idx].info.trim());
-    return self.renderToken.apply(self, arguments);
+    tokens[idx].attrJoin('class', `alert-${tokens[idx].info.trim()}`);
+    return self.renderToken(...arguments);
 }
 md.use(markdownitContainer, 'success', { render: renderContainer });
 md.use(markdownitContainer, 'info', { render: renderContainer });
@@ -966,25 +960,25 @@ md.use(markdownitContainer, 'danger', { render: renderContainer });
 
 md.renderer.rules.image = function (tokens, idx, options, env, self) {
     tokens[idx].attrJoin('class', 'raw');
-    return self.renderToken.apply(self, arguments);
+    return self.renderToken(...arguments);
 };
 md.renderer.rules.list_item_open = function (tokens, idx, options, env, self) {
     tokens[idx].attrJoin('class', 'raw');
-    return self.renderToken.apply(self, arguments);
+    return self.renderToken(...arguments);
 };
 md.renderer.rules.blockquote_open = function (tokens, idx, options, env, self) {
     tokens[idx].attrJoin('class', 'raw');
-    return self.renderToken.apply(self, arguments);
+    return self.renderToken(...arguments);
 };
 md.renderer.rules.heading_open = function (tokens, idx, options, env, self) {
     tokens[idx].attrJoin('class', 'raw');
-    return self.renderToken.apply(self, arguments);
+    return self.renderToken(...arguments);
 };
-md.renderer.rules.fence = function (tokens, idx, options, env, self) {
-    var token = tokens[idx],
-      info = token.info ? md.utils.unescapeAll(token.info).trim() : '',
-      langName = '',
-      highlighted;
+md.renderer.rules.fence = (tokens, idx, options, env, self) => {
+    const token = tokens[idx];
+    const info = token.info ? md.utils.unescapeAll(token.info).trim() : '';
+    let langName = '';
+    let highlighted;
 
     if (info) {
         langName = info.split(/\s+/g)[0];
@@ -1001,110 +995,99 @@ md.renderer.rules.fence = function (tokens, idx, options, env, self) {
     }
 
     if (highlighted.indexOf('<pre') === 0) {
-        return highlighted + '\n';
+        return `${highlighted}\n`;
     }
 
-    return  '<pre><code' + self.renderAttrs(token) + '>'
-        + highlighted
-        + '</code></pre>\n';
+    return  `<pre><code${self.renderAttrs(token)}>${highlighted}</code></pre>\n`;
 };
 
 /* Defined regex markdown it plugins */
-var Plugin = require('markdown-it-regexp');
+import Plugin from 'markdown-it-regexp';
 
 //youtube
-var youtubePlugin = new Plugin(
+const youtubePlugin = new Plugin(
     // regexp to match
     /{%youtube\s*([\d\D]*?)\s*%}/,
 
-    // this function will be called when something matches
-    function (match, utils) {
-        var videoid = match[1];
+    (match, utils) => {
+        const videoid = match[1];
         if (!videoid) return;
-        var div = $('<div class="youtube raw"></div>');
+        const div = $('<div class="youtube raw"></div>');
         div.attr('data-videoid', videoid);
-        var thumbnail_src = '//img.youtube.com/vi/' + videoid + '/hqdefault.jpg';
-        var image = '<img src="' + thumbnail_src + '" />';
+        const thumbnail_src = `//img.youtube.com/vi/${videoid}/hqdefault.jpg`;
+        const image = `<img src="${thumbnail_src}" />`;
         div.append(image);
-        var icon = '<i class="icon fa fa-youtube-play fa-5x"></i>';
+        const icon = '<i class="icon fa fa-youtube-play fa-5x"></i>';
         div.append(icon);
         return div[0].outerHTML;
     }
 );
 //vimeo
-var vimeoPlugin = new Plugin(
+const vimeoPlugin = new Plugin(
     // regexp to match
     /{%vimeo\s*([\d\D]*?)\s*%}/,
 
-    // this function will be called when something matches
-    function (match, utils) {
-        var videoid = match[1];
+    (match, utils) => {
+        const videoid = match[1];
         if (!videoid) return;
-        var div = $('<div class="vimeo raw"></div>');
+        const div = $('<div class="vimeo raw"></div>');
         div.attr('data-videoid', videoid);
-        var icon = '<i class="icon fa fa-vimeo-square fa-5x"></i>';
+        const icon = '<i class="icon fa fa-vimeo-square fa-5x"></i>';
         div.append(icon);
         return div[0].outerHTML;
     }
 );
 //gist
-var gistPlugin = new Plugin(
+const gistPlugin = new Plugin(
     // regexp to match
     /{%gist\s*([\d\D]*?)\s*%}/,
 
-    // this function will be called when something matches
-    function (match, utils) {
-        var gistid = match[1];
-        var code = '<code data-gist-id="' + gistid + '"/>';
+    (match, utils) => {
+        const gistid = match[1];
+        const code = `<code data-gist-id="${gistid}"/>`;
         return code;
     }
 );
 //TOC
-var tocPlugin = new Plugin(
+const tocPlugin = new Plugin(
     // regexp to match
     /^\[TOC\]$/i,
 
-    // this function will be called when something matches
-    function (match, utils) {
-        return '<div class="toc"></div>';
-    }
+    (match, utils) => '<div class="toc"></div>'
 );
 //slideshare
-var slidesharePlugin = new Plugin(
+const slidesharePlugin = new Plugin(
     // regexp to match
     /{%slideshare\s*([\d\D]*?)\s*%}/,
 
-    // this function will be called when something matches
-    function (match, utils) {
-        var slideshareid = match[1];
-        var div = $('<div class="slideshare raw"></div>');
+    (match, utils) => {
+        const slideshareid = match[1];
+        const div = $('<div class="slideshare raw"></div>');
         div.attr('data-slideshareid', slideshareid);
         return div[0].outerHTML;
     }
 );
 //speakerdeck
-var speakerdeckPlugin = new Plugin(
+const speakerdeckPlugin = new Plugin(
     // regexp to match
     /{%speakerdeck\s*([\d\D]*?)\s*%}/,
 
-    // this function will be called when something matches
-    function (match, utils) {
-        var speakerdeckid = match[1];
-        var div = $('<div class="speakerdeck raw"></div>');
+    (match, utils) => {
+        const speakerdeckid = match[1];
+        const div = $('<div class="speakerdeck raw"></div>');
         div.attr('data-speakerdeckid', speakerdeckid);
         return div[0].outerHTML;
     }
 );
 //pdf
-var pdfPlugin = new Plugin(
+const pdfPlugin = new Plugin(
     // regexp to match
     /{%pdf\s*([\d\D]*?)\s*%}/,
 
-    // this function will be called when something matches
-    function (match, utils) {
-        var pdfurl = match[1];
+    (match, utils) => {
+        const pdfurl = match[1];
         if (!isValidURL(pdfurl)) return match[0];
-        var div = $('<div class="pdf raw"></div>');
+        const div = $('<div class="pdf raw"></div>');
         div.attr('data-pdfurl', pdfurl);
         return div[0].outerHTML;
     }
@@ -1112,8 +1095,8 @@ var pdfPlugin = new Plugin(
 
 //yaml meta, from https://github.com/eugeneware/remarkable-meta
 function get(state, line) {
-    var pos = state.bMarks[line];
-    var max = state.eMarks[line];
+    const pos = state.bMarks[line];
+    const max = state.eMarks[line];
     return state.src.substr(pos, max - pos);
 }
 
@@ -1122,9 +1105,9 @@ function meta(state, start, end, silent) {
     if (state.tShift[start] < 0) return false;
     if (!get(state, start).match(/^---$/)) return false;
 
-    var data = [];
+    const data = [];
     for (var line = start + 1; line < end; line++) {
-        var str = get(state, line);
+        const str = get(state, line);
         if (str.match(/^(\.{3}|-{3})$/)) break;
         if (state.tShift[line] < 0) break;
         data.push(str);
@@ -1162,24 +1145,6 @@ md.use(slidesharePlugin);
 md.use(speakerdeckPlugin);
 md.use(pdfPlugin);
 
-module.exports = {
-  md: md,
-  updateLastChange: updateLastChange,
-  postProcess: postProcess,
-  finishView: finishView,
-  autoLinkify: autoLinkify,
-  deduplicatedHeaderId: deduplicatedHeaderId,
-  renderTOC: renderTOC,
-  renderTitle: renderTitle,
-  renderFilename: renderFilename,
-  renderTags: renderTags,
-  isValidURL: isValidURL,
-  generateToc: generateToc,
-  smoothHashScroll: smoothHashScroll,
-  scrollToHash: scrollToHash,
-  updateLastChangeUser: updateLastChangeUser,
-  updateOwner: updateOwner,
-  parseMeta: parseMeta,
-  exportToHTML: exportToHTML,
-  exportToRawHTML: exportToRawHTML
+export default {
+  md
 };
diff --git a/public/js/google-drive-picker.js b/public/js/google-drive-picker.js
index e653653..94aa77f 100644
--- a/public/js/google-drive-picker.js
+++ b/public/js/google-drive-picker.js
@@ -52,6 +52,7 @@
             var view = new google.picker.DocsView();
             view.setMimeTypes("text/markdown,text/html");
             view.setIncludeFolders(true);
+            view.setOwnedByMe(true);
 			this.picker = new google.picker.PickerBuilder().
                 enableFeature(google.picker.Feature.NAV_HIDDEN).
 				addView(view).
@@ -115,4 +116,4 @@
 			}, callback ? callback : function() {});
 		}
 	};
-}());
\ No newline at end of file
+}());
diff --git a/public/js/history.js b/public/js/history.js
index 6972f24..34b2cba 100644
--- a/public/js/history.js
+++ b/public/js/history.js
@@ -1,10 +1,13 @@
-var store = require('store');
-var S = require('string');
+import store from 'store';
+import S from 'string';
 
-var common = require('./common');
-var checkIfAuth = common.checkIfAuth;
-var urlpath = common.urlpath;
-var getLoginState = common.getLoginState;
+import {
+    checkIfAuth
+} from './lib/common/login';
+
+import {
+    urlpath
+} from './lib/config';
 
 window.migrateHistoryFromTempCallback = null;
 
@@ -12,22 +15,22 @@ migrateHistoryFromTemp();
 
 function migrateHistoryFromTemp() {
     if (url('#tempid')) {
-        $.get(serverurl + '/temp', {
+        $.get(`${serverurl}/temp`, {
                 tempid: url('#tempid')
             })
-            .done(function (data) {
+            .done(data => {
                 if (data && data.temp) {
-                    getStorageHistory(function (olddata) {
+                    getStorageHistory(olddata => {
                         if (!olddata || olddata.length == 0) {
                             saveHistoryToStorage(JSON.parse(data.temp));
                         }
                     });
                 }
             })
-            .always(function () {
-                var hash = location.hash.split('#')[1];
+            .always(() => {
+                let hash = location.hash.split('#')[1];
                 hash = hash.split('&');
-                for (var i = 0; i < hash.length; i++)
+                for (let i = 0; i < hash.length; i++)
                     if (hash[i].indexOf('tempid') == 0) {
                         hash.splice(i, 1);
                         i--;
@@ -40,12 +43,12 @@ function migrateHistoryFromTemp() {
     }
 }
 
-function saveHistory(notehistory) {
+export function saveHistory(notehistory) {
     checkIfAuth(
-        function () {
+        () => {
             saveHistoryToServer(notehistory);
         },
-        function () {
+        () => {
             saveHistoryToStorage(notehistory);
         }
     );
@@ -65,7 +68,7 @@ function saveHistoryToCookie(notehistory) {
 }
 
 function saveHistoryToServer(notehistory) {
-    $.post(serverurl + '/history', {
+    $.post(`${serverurl}/history`, {
         history: JSON.stringify(notehistory)
     });
 }
@@ -75,37 +78,37 @@ function saveCookieHistoryToStorage(callback) {
     callback();
 }
 
-function saveStorageHistoryToServer(callback) {
-    var data = store.get('notehistory');
+export function saveStorageHistoryToServer(callback) {
+    const data = store.get('notehistory');
     if (data) {
-        $.post(serverurl + '/history', {
+        $.post(`${serverurl}/history`, {
                 history: data
             })
-            .done(function (data) {
+            .done(data => {
                 callback(data);
             });
     }
 }
 
 function saveCookieHistoryToServer(callback) {
-    $.post(serverurl + '/history', {
+    $.post(`${serverurl}/history`, {
             history: Cookies.get('notehistory')
         })
-        .done(function (data) {
+        .done(data => {
             callback(data);
         });
 }
 
-function clearDuplicatedHistory(notehistory) {
-    var newnotehistory = [];
-    for (var i = 0; i < notehistory.length; i++) {
-        var found = false;
-        for (var j = 0; j < newnotehistory.length; j++) {
-            var id = notehistory[i].id.replace(/\=+$/, '');
-            var newId = newnotehistory[j].id.replace(/\=+$/, '');
+export function clearDuplicatedHistory(notehistory) {
+    const newnotehistory = [];
+    for (let i = 0; i < notehistory.length; i++) {
+        let found = false;
+        for (let j = 0; j < newnotehistory.length; j++) {
+            const id = notehistory[i].id.replace(/\=+$/, '');
+            const newId = newnotehistory[j].id.replace(/\=+$/, '');
             if (id == newId || notehistory[i].id == newnotehistory[j].id || !notehistory[i].id || !newnotehistory[j].id) {
-                var time = (typeof notehistory[i].time === 'number' ? moment(notehistory[i].time) : moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a'));
-                var newTime = (typeof newnotehistory[i].time === 'number' ? moment(newnotehistory[i].time) : moment(newnotehistory[i].time, 'MMMM Do YYYY, h:mm:ss a'));
+                const time = (typeof notehistory[i].time === 'number' ? moment(notehistory[i].time) : moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a'));
+                const newTime = (typeof newnotehistory[i].time === 'number' ? moment(newnotehistory[i].time) : moment(newnotehistory[i].time, 'MMMM Do YYYY, h:mm:ss a'));
                 if(time >= newTime) {
                     newnotehistory[j] = notehistory[i];
                 }
@@ -123,42 +126,42 @@ function addHistory(id, text, time, tags, pinned, notehistory) {
     // only add when note id exists
     if (id) {
       notehistory.push({
-          id: id,
-          text: text,
-          time: time,
-          tags: tags,
-          pinned: pinned
+          id,
+          text,
+          time,
+          tags,
+          pinned
       });
     }
     return notehistory;
 }
 
-function removeHistory(id, notehistory) {
-    for (var i = 0; i < notehistory.length; i++) {
+export function removeHistory(id, notehistory) {
+    for (let i = 0; i < notehistory.length; i++) {
         if (notehistory[i].id == id) {
             notehistory.splice(i, 1);
-			i--;
-		}
+            i -= 1;
+        }
     }
     return notehistory;
 }
 
 //used for inner
-function writeHistory(title, tags) {
+export function writeHistory(title, tags) {
     checkIfAuth(
-        function () {
+        () => {
             // no need to do this anymore, this will count from server-side
             // writeHistoryToServer(title, tags);
         },
-        function () {
+        () => {
             writeHistoryToStorage(title, tags);
         }
     );
 }
 
 function writeHistoryToServer(title, tags) {
-    $.get(serverurl + '/history')
-        .done(function (data) {
+    $.get(`${serverurl}/history`)
+        .done(data => {
             try {
                 if (data.history) {
                     var notehistory = data.history;
@@ -171,10 +174,10 @@ function writeHistoryToServer(title, tags) {
             if (!notehistory)
                 notehistory = [];
 
-            var newnotehistory = generateHistory(title, tags, notehistory);
+            const newnotehistory = generateHistory(title, tags, notehistory);
             saveHistoryToServer(newnotehistory);
         })
-        .fail(function (xhr, status, error) {
+        .fail((xhr, status, error) => {
             console.error(xhr.responseText);
         });
 }
@@ -188,13 +191,13 @@ function writeHistoryToCookie(title, tags) {
     if (!notehistory)
         notehistory = [];
 
-    var newnotehistory = generateHistory(title, tags, notehistory);
+    const newnotehistory = generateHistory(title, tags, notehistory);
     saveHistoryToCookie(newnotehistory);
 }
 
 function writeHistoryToStorage(title, tags) {
     if (store.enabled) {
-        var data = store.get('notehistory');
+        let data = store.get('notehistory');
         if (data) {
             if (typeof data == "string")
                 data = JSON.parse(data);
@@ -204,7 +207,7 @@ function writeHistoryToStorage(title, tags) {
         if (!notehistory)
             notehistory = [];
 
-        var newnotehistory = generateHistory(title, tags, notehistory);
+        const newnotehistory = generateHistory(title, tags, notehistory);
         saveHistoryToStorage(newnotehistory);
     } else {
         writeHistoryToCookie(title, tags);
@@ -212,32 +215,30 @@ function writeHistoryToStorage(title, tags) {
 }
 
 if (!Array.isArray) {
-    Array.isArray = function(arg) {
-        return Object.prototype.toString.call(arg) === '[object Array]';
-    };
+    Array.isArray = arg => Object.prototype.toString.call(arg) === '[object Array]';
 }
 
 function renderHistory(title, tags) {
     //console.debug(tags);
-    var id = urlpath ? location.pathname.slice(urlpath.length + 1, location.pathname.length).split('/')[1] : location.pathname.split('/')[1];
+    const id = urlpath ? location.pathname.slice(urlpath.length + 1, location.pathname.length).split('/')[1] : location.pathname.split('/')[1];
     return {
-        id: id,
+        id,
         text: title,
         time: moment().valueOf(),
-        tags: tags
+        tags
     };
 }
 
 function generateHistory(title, tags, notehistory) {
-    var info = renderHistory(title, tags);
-	//keep any pinned data
-	var pinned = false;
-	for (var i = 0; i < notehistory.length; i++) {
-		if (notehistory[i].id == info.id && notehistory[i].pinned) {
-			pinned = true;
-			break;
-		}
-	}
+    const info = renderHistory(title, tags);
+    //keep any pinned data
+    let pinned = false;
+    for (let i = 0; i < notehistory.length; i++) {
+        if (notehistory[i].id == info.id && notehistory[i].pinned) {
+            pinned = true;
+            break;
+        }
+    }
     notehistory = removeHistory(info.id, notehistory);
     notehistory = addHistory(info.id, info.text, info.time, info.tags, pinned, notehistory);
     notehistory = clearDuplicatedHistory(notehistory);
@@ -245,25 +246,25 @@ function generateHistory(title, tags, notehistory) {
 }
 
 //used for outer
-function getHistory(callback) {
+export function getHistory(callback) {
     checkIfAuth(
-        function () {
+        () => {
             getServerHistory(callback);
         },
-        function () {
+        () => {
             getStorageHistory(callback);
         }
     );
 }
 
 function getServerHistory(callback) {
-    $.get(serverurl + '/history')
-        .done(function (data) {
+    $.get(`${serverurl}/history`)
+        .done(data => {
             if (data.history) {
                 callback(data.history);
             }
         })
-        .fail(function (xhr, status, error) {
+        .fail((xhr, status, error) => {
             console.error(xhr.responseText);
         });
 }
@@ -272,9 +273,9 @@ function getCookieHistory(callback) {
     callback(Cookies.getJSON('notehistory'));
 }
 
-function getStorageHistory(callback) {
+export function getStorageHistory(callback) {
     if (store.enabled) {
-        var data = store.get('notehistory');
+        let data = store.get('notehistory');
         if (data) {
             if (typeof data == "string")
                 data = JSON.parse(data);
@@ -286,37 +287,37 @@ function getStorageHistory(callback) {
     }
 }
 
-function parseHistory(list, callback) {
+export function parseHistory(list, callback) {
     checkIfAuth(
-        function () {
+        () => {
             parseServerToHistory(list, callback);
         },
-        function () {
+        () => {
             parseStorageToHistory(list, callback);
         }
     );
 }
 
-function parseServerToHistory(list, callback) {
-    $.get(serverurl + '/history')
-        .done(function (data) {
+export function parseServerToHistory(list, callback) {
+    $.get(`${serverurl}/history`)
+        .done(data => {
             if (data.history) {
                 parseToHistory(list, data.history, callback);
             }
         })
-        .fail(function (xhr, status, error) {
+        .fail((xhr, status, error) => {
             console.error(xhr.responseText);
         });
 }
 
 function parseCookieToHistory(list, callback) {
-    var notehistory = Cookies.getJSON('notehistory');
+    const notehistory = Cookies.getJSON('notehistory');
     parseToHistory(list, notehistory, callback);
 }
 
-function parseStorageToHistory(list, callback) {
+export function parseStorageToHistory(list, callback) {
     if (store.enabled) {
-        var data = store.get('notehistory');
+        let data = store.get('notehistory');
         if (data) {
             if (typeof data == "string")
                 data = JSON.parse(data);
@@ -332,9 +333,9 @@ function parseToHistory(list, notehistory, callback) {
     if (!callback) return;
     else if (!list || !notehistory) callback(list, notehistory);
     else if (notehistory && notehistory.length > 0) {
-        for (var i = 0; i < notehistory.length; i++) {
+        for (let i = 0; i < notehistory.length; i++) {
             //parse time to timestamp and fromNow
-            var timestamp = (typeof notehistory[i].time === 'number' ? moment(notehistory[i].time) : moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a'));
+            const timestamp = (typeof notehistory[i].time === 'number' ? moment(notehistory[i].time) : moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a'));
             notehistory[i].timestamp = timestamp.valueOf();
             notehistory[i].fromNow = timestamp.fromNow();
             notehistory[i].time = timestamp.format('llll');
@@ -349,42 +350,23 @@ function parseToHistory(list, notehistory, callback) {
     callback(list, notehistory);
 }
 
-function postHistoryToServer(noteId, data, callback) {
-    $.post(serverurl + '/history/' + noteId, data)
-    .done(function (result) {
-        return callback(null, result);
-    })
-    .fail(function (xhr, status, error) {
+export function postHistoryToServer(noteId, data, callback) {
+    $.post(`${serverurl}/history/${noteId}`, data)
+    .done(result => callback(null, result))
+    .fail((xhr, status, error) => {
         console.error(xhr.responseText);
         return callback(error, null);
     });
 }
 
-function deleteServerHistory(noteId, callback) {
+export function deleteServerHistory(noteId, callback) {
     $.ajax({
-        url: serverurl + '/history' + (noteId ? '/' + noteId : ""),
+        url: `${serverurl}/history${noteId ? '/' + noteId : ""}`,
         type: 'DELETE'
     })
-    .done(function (result) {
-        return callback(null, result);
-    })
-    .fail(function (xhr, status, error) {
+    .done(result => callback(null, result))
+    .fail((xhr, status, error) => {
         console.error(xhr.responseText);
         return callback(error, null);
     });
 }
-
-module.exports = {
-    writeHistory: writeHistory,
-    parseHistory: parseHistory,
-    getStorageHistory: getStorageHistory,
-    getHistory: getHistory,
-    saveHistory: saveHistory,
-    removeHistory: removeHistory,
-    parseStorageToHistory: parseStorageToHistory,
-    postHistoryToServer: postHistoryToServer,
-    deleteServerHistory: deleteServerHistory,
-    parseServerToHistory: parseServerToHistory,
-    saveStorageHistoryToServer: saveStorageHistoryToServer,
-    clearDuplicatedHistory: clearDuplicatedHistory
-}
diff --git a/public/js/index.js b/public/js/index.js
index a018e51..f0c476e 100644
--- a/public/js/index.js
+++ b/public/js/index.js
@@ -17,51 +17,58 @@ var _ = require("lodash");
 
 var List = require('list.js');
 
-var common = require('./common.js');
-var urlpath = common.urlpath;
-var noteid = common.noteid;
-var debug = common.debug;
-var version = common.version;
-var GOOGLE_API_KEY = common.GOOGLE_API_KEY;
-var GOOGLE_CLIENT_ID = common.GOOGLE_CLIENT_ID;
-var DROPBOX_APP_KEY = common.DROPBOX_APP_KEY;
-var noteurl = common.noteurl;
+import {
+    checkLoginStateChanged,
+    setloginStateChangeEvent
+} from './lib/common/login';
 
-var checkLoginStateChanged = common.checkLoginStateChanged;
+import {
+    debug,
+    DROPBOX_APP_KEY,
+    GOOGLE_API_KEY,
+    GOOGLE_CLIENT_ID,
+    noteid,
+    noteurl,
+    urlpath,
+    version
+} from './lib/config';
 
-var extra = require('./extra');
-var md = extra.md;
-var updateLastChange = extra.updateLastChange;
-var postProcess = extra.postProcess;
-var finishView = extra.finishView;
-var autoLinkify = extra.autoLinkify;
-var generateToc = extra.generateToc;
-var smoothHashScroll = extra.smoothHashScroll;
-var deduplicatedHeaderId = extra.deduplicatedHeaderId;
-var renderTOC = extra.renderTOC;
-var renderTitle = extra.renderTitle;
-var renderFilename = extra.renderFilename;
-var renderTags = extra.renderTags;
-var isValidURL = extra.isValidURL;
-var scrollToHash = extra.scrollToHash;
-var updateLastChangeUser = extra.updateLastChangeUser;
-var updateOwner = extra.updateOwner;
-var parseMeta = extra.parseMeta;
-var exportToHTML = extra.exportToHTML;
-var exportToRawHTML = extra.exportToRawHTML;
+import {
+    autoLinkify,
+    deduplicatedHeaderId,
+    exportToHTML,
+    exportToRawHTML,
+    finishView,
+    generateToc,
+    isValidURL,
+    md,
+    parseMeta,
+    postProcess,
+    renderFilename,
+    renderTOC,
+    renderTags,
+    renderTitle,
+    scrollToHash,
+    smoothHashScroll,
+    updateLastChange,
+    updateLastChangeUser,
+    updateOwner
+} from './extra';
 
-var syncScroll = require('./syncscroll');
-var setupSyncAreas = syncScroll.setupSyncAreas;
-var clearMap = syncScroll.clearMap;
-var syncScrollToEdit = syncScroll.syncScrollToEdit;
-var syncScrollToView = syncScroll.syncScrollToView;
+import {
+    clearMap,
+    setupSyncAreas,
+    syncScrollToEdit,
+    syncScrollToView
+} from './syncscroll';
 
-var historyModule = require('./history');
-var writeHistory = historyModule.writeHistory;
-var deleteServerHistory = historyModule.deleteServerHistory;
-var getHistory = historyModule.getHistory;
-var saveHistory = historyModule.saveHistory;
-var removeHistory = historyModule.removeHistory;
+import {
+    writeHistory,
+    deleteServerHistory,
+    getHistory,
+    saveHistory,
+    removeHistory
+} from './history';
 
 var renderer = require('./render');
 var preventXSS = renderer.preventXSS;
@@ -401,7 +408,8 @@ window.lastInfo = {
         cursor: {
             line: null,
             ch: null
-        }
+        },
+        selections: null
     },
     view: {
         scroll: {
@@ -963,10 +971,10 @@ function setNeedRefresh() {
     showStatus(statusType.offline);
 }
 
-loginStateChangeEvent = function () {
+setloginStateChangeEvent(function () {
     setRefreshModal('user-state-changed');
     setNeedRefresh();
-};
+});
 
 //visibility
 var wasFocus = false;
@@ -1535,7 +1543,7 @@ ui.toolbar.download.markdown.click(function (e) {
     var blob = new Blob([markdown], {
         type: "text/markdown;charset=utf-8"
     });
-    saveAs(blob, filename);
+    saveAs(blob, filename, true);
 });
 //html
 ui.toolbar.download.html.click(function (e) {
@@ -1915,7 +1923,7 @@ $('#revisionModalDownload').click(function () {
     var blob = new Blob([revision.content], {
         type: "text/markdown;charset=utf-8"
     });
-    saveAs(blob, filename);
+    saveAs(blob, filename, true);
 });
 $('#revisionModalRevert').click(function () {
     if (!revision) return;
@@ -2511,7 +2519,7 @@ var addStyleRule = (function () {
 }());
 function updateAuthorshipInner() {
     // ignore when ot not synced yet
-    if (cmClient && Object.keys(cmClient.state).length > 0) return;
+    if (havePendingOperation()) return;
     authorMarks = {};
     for (var i = 0; i < authorship.length; i++) {
         var atom = authorship[i];
@@ -2668,7 +2676,7 @@ editor.on('update', function () {
     });
     // clear tooltip which described element has been removed
     $('[id^="tooltip"]').each(function (index, element) {
-        $ele = $(element);
+        var $ele = $(element);
         if ($('[aria-describedby="' + $ele.attr('id') + '"]').length <= 0) $ele.remove();
     });
 });
@@ -2726,12 +2734,16 @@ var EditorClient = ot.EditorClient;
 var SocketIOAdapter = ot.SocketIOAdapter;
 var CodeMirrorAdapter = ot.CodeMirrorAdapter;
 var cmClient = null;
+var synchronized_ = null;
+
+function havePendingOperation() {
+    return (cmClient && cmClient.state && cmClient.state.hasOwnProperty('outstanding')) ? true : false;
+}
 
 socket.on('doc', function (obj) {
     var body = obj.str;
     var bodyMismatch = editor.getValue() !== body;
-    var havePendingOperation = cmClient && Object.keys(cmClient.state).length > 0;
-    var setDoc = !cmClient || (cmClient && (cmClient.revision === -1 || (cmClient.revision !== obj.revision && !havePendingOperation))) || obj.force;
+    var setDoc = !cmClient || (cmClient && (cmClient.revision === -1 || (cmClient.revision !== obj.revision && !havePendingOperation()))) || obj.force;
 
     saveInfo();
     if (setDoc && bodyMismatch) {
@@ -2756,16 +2768,17 @@ socket.on('doc', function (obj) {
             obj.revision, obj.clients,
             new SocketIOAdapter(socket), new CodeMirrorAdapter(editor)
         );
+        synchronized_ = cmClient.state;
     } else if (setDoc) {
         if (bodyMismatch) {
             cmClient.undoManager.undoStack.length = 0;
             cmClient.undoManager.redoStack.length = 0;
         }
         cmClient.revision = obj.revision;
-        cmClient.setState(new ot.Client.Synchronized());
+        cmClient.setState(synchronized_);
         cmClient.initializeClientList();
         cmClient.initializeClients(obj.clients);
-    } else if (havePendingOperation) {
+    } else if (havePendingOperation()) {
         cmClient.serverReconnect();
     }
 
@@ -3387,6 +3400,7 @@ function saveInfo() {
             break;
     }
     lastInfo.edit.cursor = editor.getCursor();
+    lastInfo.edit.selections = editor.listSelections();
     lastInfo.needRestore = true;
 }
 
@@ -3396,6 +3410,7 @@ function restoreInfo() {
         var line = lastInfo.edit.cursor.line;
         var ch = lastInfo.edit.cursor.ch;
         editor.setCursor(line, ch);
+        editor.setSelections(lastInfo.edit.selections);
         switch (currentMode) {
             case modeType.edit:
                 if (scrollbarStyle == 'native') {
@@ -3445,6 +3460,7 @@ function updateViewInner() {
     var value = editor.getValue();
     var lastMeta = md.meta;
     md.meta = {};
+    delete md.metaError;
     var rendered = md.render(value);
     if (md.meta.type && md.meta.type === 'slide') {
         var slideOptions = {
@@ -3716,6 +3732,7 @@ function checkCursorMenuInner() {
     var offsetLeft = 0;
     var offsetTop = defaultTextHeight;
     // set up side down
+    window.upSideDown = false;
     var lastUpSideDown = upSideDown = false;
     // only do when have width and height
     if (width > 0 && height > 0) {
@@ -3944,7 +3961,7 @@ $(editor.getInputField())
             match: /(?:^|\n|\s)(\>.*|\s|)((\^|)\[(\^|)\](\[\]|\(\)|\:|)\s*\w*)$/,
             search: function (term, callback) {
                 var line = editor.getLine(editor.getCursor().line);
-                quote = line.match(this.match)[1].trim();
+                var quote = line.match(this.match)[1].trim();
                 var list = [];
                 if (quote.indexOf('>') == 0) {
                     $.map(supportExtraTags, function (extratag) {
diff --git a/public/js/lib/common/login.js b/public/js/lib/common/login.js
new file mode 100644
index 0000000..58fa55c
--- /dev/null
+++ b/public/js/lib/common/login.js
@@ -0,0 +1,89 @@
+import { serverurl } from '../config';
+
+let checkAuth = false;
+let profile = null;
+let lastLoginState = getLoginState();
+let lastUserId = getUserId();
+var loginStateChangeEvent = null;
+
+export function setloginStateChangeEvent(func) {
+    loginStateChangeEvent = func;
+}
+
+export function resetCheckAuth() {
+    checkAuth = false;
+}
+
+export function setLoginState(bool, id) {
+    Cookies.set('loginstate', bool, {
+        expires: 365
+    });
+    if (id) {
+        Cookies.set('userid', id, {
+            expires: 365
+        });
+    } else {
+        Cookies.remove('userid');
+    }
+    lastLoginState = bool;
+    lastUserId = id;
+    checkLoginStateChanged();
+}
+
+export function checkLoginStateChanged() {
+    if (getLoginState() != lastLoginState || getUserId() != lastUserId) {
+        if (loginStateChangeEvent) setTimeout(loginStateChangeEvent, 100);
+        return true;
+    } else {
+        return false;
+    }
+}
+
+export function getLoginState() {
+    const state = Cookies.get('loginstate');
+    return state === "true" || state === true;
+}
+
+export function getUserId() {
+    return Cookies.get('userid');
+}
+
+export function clearLoginState() {
+    Cookies.remove('loginstate');
+}
+
+export function checkIfAuth(yesCallback, noCallback) {
+    const cookieLoginState = getLoginState();
+    if (checkLoginStateChanged()) checkAuth = false;
+    if (!checkAuth || typeof cookieLoginState == 'undefined') {
+        $.get(`${serverurl}/me`)
+            .done(data => {
+                if (data && data.status == 'ok') {
+                    profile = data;
+                    yesCallback(profile);
+                    setLoginState(true, data.id);
+                } else {
+                    noCallback();
+                    setLoginState(false);
+                }
+            })
+            .fail(() => {
+                noCallback();
+            })
+            .always(() => {
+                checkAuth = true;
+            });
+    } else if (cookieLoginState) {
+        yesCallback(profile);
+    } else {
+        noCallback();
+    }
+}
+
+export default {
+    checkAuth,
+    profile,
+    lastLoginState,
+    lastUserId,
+    loginStateChangeEvent
+};
diff --git a/public/js/lib/config/index.js b/public/js/lib/config/index.js
new file mode 100644
index 0000000..2b73679
--- /dev/null
+++ b/public/js/lib/config/index.js
@@ -0,0 +1,19 @@
+import configJson from '../../../../config.json'; // root path json config
+
+const config = 'production' === process.env.NODE_ENV ? configJson.production : configJson.development;
+
+export const GOOGLE_API_KEY = (config.google && config.google.apiKey) || '';
+export const GOOGLE_CLIENT_ID = (config.google && config.google.clientID) || '';
+export const DROPBOX_APP_KEY = (config.dropbox && config.dropbox.appKey) || '';
+
+export const domain = config.domain || ''; // domain name
+export const urlpath = config.urlpath || ''; // sub url path, like: www.example.com/<urlpath>
+export const debug = config.debug || false;
+
+export const port = window.location.port;
+export const serverurl = `${window.location.protocol}//${domain ? domain : window.location.hostname}${port ? ':' + port : ''}${urlpath ? '/' + urlpath : ''}`;
+window.serverurl = serverurl;
+export const noteid = urlpath ? window.location.pathname.slice(urlpath.length + 1, window.location.pathname.length).split('/')[1] : window.location.pathname.split('/')[1];
+export const noteurl = `${serverurl}/${noteid}`;
+
+export const version = '0.5.0';
diff --git a/public/js/pretty.js b/public/js/pretty.js
index c1a471a..18d0dc0 100644
--- a/public/js/pretty.js
+++ b/public/js/pretty.js
@@ -4,31 +4,34 @@ require('../css/site.css');
 
 require('highlight.js/styles/github-gist.css');
 
-var extra = require('./extra');
-var md = extra.md;
-var finishView = extra.finishView;
-var autoLinkify = extra.autoLinkify;
-var deduplicatedHeaderId = extra.deduplicatedHeaderId;
-var renderTOC = extra.renderTOC;
-var generateToc = extra.generateToc;
-var smoothHashScroll = extra.smoothHashScroll;
-var postProcess = extra.postProcess;
-var updateLastChange = extra.updateLastChange;
-var parseMeta = extra.parseMeta;
-var scrollToHash = extra.scrollToHash;
-var preventXSS = require('./render').preventXSS;
+import {
+    autoLinkify,
+    deduplicatedHeaderId,
+    finishView,
+    generateToc,
+    md,
+    parseMeta,
+    postProcess,
+    renderTOC,
+    scrollToHash,
+    smoothHashScroll,
+    updateLastChange
+} from './extra';
 
-var markdown = $("#doc.markdown-body");
-var text = markdown.text();
-var lastMeta = md.meta;
+import { preventXSS } from './render';
+
+const markdown = $("#doc.markdown-body");
+const text = markdown.text();
+const lastMeta = md.meta;
 md.meta = {};
-var rendered = md.render(text);
+delete md.metaError;
+let rendered = md.render(text);
 if (md.meta.type && md.meta.type === 'slide') {
-    var slideOptions = {
+    const slideOptions = {
         separator: '^(\r\n?|\n)---(\r\n?|\n)$',
         verticalSeparator: '^(\r\n?|\n)----(\r\n?|\n)$'
     };
-    var slides = RevealMarkdown.slidify(text, slideOptions);
+    const slides = RevealMarkdown.slidify(text, slideOptions);
     markdown.html(slides);
     RevealMarkdown.initialize();
     // prevent XSS
@@ -46,10 +49,11 @@ if (md.meta.type && md.meta.type === 'slide') {
     }
     // prevent XSS
     rendered = preventXSS(rendered);
-    var result = postProcess(rendered);
+    const result = postProcess(rendered);
     markdown.html(result.html());
 }
 $(document.body).show();
+
 finishView(markdown);
 autoLinkify(markdown);
 deduplicatedHeaderId(markdown);
@@ -60,17 +64,18 @@ smoothHashScroll();
 createtime = lastchangeui.time.attr('data-createtime');
 lastchangetime = lastchangeui.time.attr('data-updatetime');
 updateLastChange();
-var url = window.location.pathname;
-$('.ui-edit').attr('href', url + '/edit');
-var toc = $('.ui-toc');
-var tocAffix = $('.ui-affix-toc');
-var tocDropdown = $('.ui-toc-dropdown');
+
+const url = window.location.pathname;
+$('.ui-edit').attr('href', `${url}/edit`);
+const toc = $('.ui-toc');
+const tocAffix = $('.ui-affix-toc');
+const tocDropdown = $('.ui-toc-dropdown');
 //toc
-tocDropdown.click(function (e) {
+tocDropdown.click(e => {
     e.stopPropagation();
 });
 
-var enoughForAffixToc = true;
+let enoughForAffixToc = true;
 
 function generateScrollspy() {
     $(document.body).scrollspy({
@@ -89,18 +94,18 @@ function generateScrollspy() {
 
 function windowResize() {
     //toc right
-    var paddingRight = parseFloat(markdown.css('padding-right'));
-    var right = ($(window).width() - (markdown.offset().left + markdown.outerWidth() - paddingRight));
-    toc.css('right', right + 'px');
+    const paddingRight = parseFloat(markdown.css('padding-right'));
+    const right = ($(window).width() - (markdown.offset().left + markdown.outerWidth() - paddingRight));
+    toc.css('right', `${right}px`);
     //affix toc left
-    var newbool;
-    var rightMargin = (markdown.parent().outerWidth() - markdown.outerWidth()) / 2;
+    let newbool;
+    const rightMargin = (markdown.parent().outerWidth() - markdown.outerWidth()) / 2;
     //for ipad or wider device
     if (rightMargin >= 133) {
         newbool = true;
-        var affixLeftMargin = (tocAffix.outerWidth() - tocAffix.width()) / 2;
-        var left = markdown.offset().left + markdown.outerWidth() - affixLeftMargin;
-        tocAffix.css('left', left + 'px');
+        const affixLeftMargin = (tocAffix.outerWidth() - tocAffix.width()) / 2;
+        const left = markdown.offset().left + markdown.outerWidth() - affixLeftMargin;
+        tocAffix.css('left', `${left}px`);
     } else {
         newbool = false;
     }
@@ -109,10 +114,10 @@ function windowResize() {
         generateScrollspy();
     }
 }
-$(window).resize(function () {
+$(window).resize(() => {
     windowResize();
 });
-$(document).ready(function () {
+$(document).ready(() => {
     windowResize();
     generateScrollspy();
     setTimeout(scrollToHash, 0);
@@ -120,13 +125,13 @@ $(document).ready(function () {
     $('[data-toggle="tooltip"]').tooltip();
 });
 
-function scrollToTop() {
+export function scrollToTop() {
     $('body, html').stop(true, true).animate({
         scrollTop: 0
     }, 100, "linear");
 }
 
-function scrollToBottom() {
+export function scrollToBottom() {
     $('body, html').stop(true, true).animate({
         scrollTop: $(document.body)[0].scrollHeight
     }, 100, "linear");
@@ -134,8 +139,3 @@ function scrollToBottom() {
 
 window.scrollToTop = scrollToTop;
 window.scrollToBottom = scrollToBottom;
-
-module.exports = {
-  scrollToBottom: scrollToBottom,
-  scrollToTop: scrollToTop
-}
diff --git a/public/js/render.js b/public/js/render.js
index a61fc8f..5d6d0aa 100644
--- a/public/js/render.js
+++ b/public/js/render.js
@@ -9,6 +9,8 @@ var dataUriRegex = /^\s*data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base
 var whiteList = filterXSS.whiteList;
 // allow ol specify start number
 whiteList['ol'] = ['start'];
+// allow li specify value number
+whiteList['li'] = ['value'];
 // allow style tag
 whiteList['style'] = [];
 // allow kbd tag
diff --git a/public/js/slide.js b/public/js/slide.js
index 1ff388a..63cf64c 100644
--- a/public/js/slide.js
+++ b/public/js/slide.js
@@ -1,67 +1,65 @@
 require('../css/extra.css');
 require('../css/site.css');
 
-var extraModule = require('./extra');
-var md = extraModule.md;
-var updateLastChange = extraModule.updateLastChange;
-var finishView = extraModule.finishView;
+import { md, updateLastChange, finishView } from './extra';
 
-var preventXSS = require('./render').preventXSS;
+import { preventXSS } from './render';
 
-var body = $(".slides").text();
+const body = $(".slides").text();
 
 createtime = lastchangeui.time.attr('data-createtime');
 lastchangetime = lastchangeui.time.attr('data-updatetime');
 updateLastChange();
-var url = window.location.pathname;
-$('.ui-edit').attr('href', url + '/edit');
+const url = window.location.pathname;
+$('.ui-edit').attr('href', `${url}/edit`);
 
-$(document).ready(function () {
+$(document).ready(() => {
     //tooltip
     $('[data-toggle="tooltip"]').tooltip();
 });
 
 function extend() {
-    var target = {};
-    for (var i = 0; i < arguments.length; i++) {
-        var source = arguments[i];
-        for (var key in source) {
+    const target = {};
+
+    for (const source of arguments) {
+        for (const key in source) {
             if (source.hasOwnProperty(key)) {
                 target[key] = source[key];
             }
         }
     }
+
     return target;
 }
 
 // Optional libraries used to extend on reveal.js
-var deps = [{
-    src: serverurl + '/build/reveal.js/lib/js/classList.js',
-    condition: function() {
+const deps = [{
+    src: `${serverurl}/build/reveal.js/lib/js/classList.js`,
+    condition() {
         return !document.body.classList;
     }
 }, {
-    src: serverurl + '/js/reveal-markdown.js',
-    callback: function () {
-        var slideOptions = {
+    src: `${serverurl}/js/reveal-markdown.js`,
+    callback() {
+        const slideOptions = {
             separator: '^(\r\n?|\n)---(\r\n?|\n)$',
             verticalSeparator: '^(\r\n?|\n)----(\r\n?|\n)$'
         };
-        var slides = RevealMarkdown.slidify(body, slideOptions);
+        const slides = RevealMarkdown.slidify(body, slideOptions);
         $(".slides").html(slides);
         RevealMarkdown.initialize();
         $(".slides").show();
     }
 }, {
-    src: serverurl + '/build/reveal.js/plugin/notes/notes.js',
+    src: `${serverurl}/build/reveal.js/plugin/notes/notes.js`,
     async: true,
-    condition: function() {
+    condition() {
         return !!document.body.classList;
     }
 }];
 
 // default options to init reveal.js
-var defaultOptions = {
+const defaultOptions = {
     controls: true,
     progress: true,
     slideNumber: true,
@@ -72,10 +70,10 @@ var defaultOptions = {
 };
 
 // options from yaml meta
-var meta = JSON.parse($("#meta").text());
+const meta = JSON.parse($("#meta").text());
 var options = meta.slideOptions || {};
 
-var view = $('.reveal');
+const view = $('.reveal');
 
 //text language
 if (meta.lang && typeof meta.lang == "string") {
@@ -97,24 +95,24 @@ if (typeof meta.breaks === 'boolean' && !meta.breaks) {
 }
 
 // options from URL query string
-var queryOptions = Reveal.getQueryHash() || {};
+const queryOptions = Reveal.getQueryHash() || {};
 
 var options = extend(defaultOptions, options, queryOptions);
 Reveal.initialize(options);
 
-window.viewAjaxCallback = function () {
+window.viewAjaxCallback = () => {
     Reveal.layout();
 };
 
 function renderSlide(event) {
     if (window.location.search.match( /print-pdf/gi )) {
-        var slides = $('.slides');
+        const slides = $('.slides');
         var title = document.title;
         finishView(slides);
         document.title = title;
         Reveal.layout();
     } else {
-        var markdown = $(event.currentSlide);
+        const markdown = $(event.currentSlide);
         if (!markdown.attr('data-rendered')) {
             var title = document.title;
             finishView(markdown);
@@ -125,16 +123,16 @@ function renderSlide(event) {
     }
 }
 
-Reveal.addEventListener('ready', function (event) {
+Reveal.addEventListener('ready', event => {
     renderSlide(event);
-    var markdown = $(event.currentSlide);
+    const markdown = $(event.currentSlide);
     // force browser redraw
-    setTimeout(function () {
+    setTimeout(() => {
         markdown.hide().show(0);
     }, 0);
 });
 Reveal.addEventListener('slidechanged', renderSlide);
 
-var isMacLike = navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i) ? true : false;
+const isMacLike = navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i) ? true : false;
 
 if (!isMacLike) $('.container').addClass('hidescrollbar');
diff --git a/public/js/syncscroll.js b/public/js/syncscroll.js
index 47d0e1c..c969317 100644
--- a/public/js/syncscroll.js
+++ b/public/js/syncscroll.js
@@ -1,12 +1,13 @@
 // Inject line numbers for sync scroll.
 
-var extra = require('./extra');
-var md = extra.md;
+import markdownitContainer from 'markdown-it-container';
+
+import { md } from './extra';
 
 function addPart(tokens, idx) {
     if (tokens[idx].map && tokens[idx].level === 0) {
-        var startline = tokens[idx].map[0] + 1;
-        var endline = tokens[idx].map[1];
+        const startline = tokens[idx].map[0] + 1;
+        const endline = tokens[idx].map[1];
         tokens[idx].attrJoin('class', 'part');
         tokens[idx].attrJoin('data-startline', startline);
         tokens[idx].attrJoin('data-endline', endline);
@@ -16,48 +17,48 @@ function addPart(tokens, idx) {
 md.renderer.rules.blockquote_open = function (tokens, idx, options, env, self) {
     tokens[idx].attrJoin('class', 'raw');
     addPart(tokens, idx);
-    return self.renderToken.apply(self, arguments);
+    return self.renderToken(...arguments);
 };
 md.renderer.rules.table_open = function (tokens, idx, options, env, self) {
     addPart(tokens, idx);
-    return self.renderToken.apply(self, arguments);
+    return self.renderToken(...arguments);
 };
 md.renderer.rules.bullet_list_open = function (tokens, idx, options, env, self) {
     addPart(tokens, idx);
-    return self.renderToken.apply(self, arguments);
+    return self.renderToken(...arguments);
 };
 md.renderer.rules.list_item_open = function (tokens, idx, options, env, self) {
     tokens[idx].attrJoin('class', 'raw');
     if (tokens[idx].map) {
-        var startline = tokens[idx].map[0] + 1;
-        var endline = tokens[idx].map[1];
+        const startline = tokens[idx].map[0] + 1;
+        const endline = tokens[idx].map[1];
         tokens[idx].attrJoin('data-startline', startline);
         tokens[idx].attrJoin('data-endline', endline);
     }
-    return self.renderToken.apply(self, arguments);
+    return self.renderToken(...arguments);
 };
 md.renderer.rules.ordered_list_open = function (tokens, idx, options, env, self) {
     addPart(tokens, idx);
-    return self.renderToken.apply(self, arguments);
+    return self.renderToken(...arguments);
 };
 md.renderer.rules.link_open = function (tokens, idx, options, env, self) {
     addPart(tokens, idx);
-    return self.renderToken.apply(self, arguments);
+    return self.renderToken(...arguments);
 };
 md.renderer.rules.paragraph_open = function (tokens, idx, options, env, self) {
     addPart(tokens, idx);
-    return self.renderToken.apply(self, arguments);
+    return self.renderToken(...arguments);
 };
 md.renderer.rules.heading_open = function (tokens, idx, options, env, self) {
     tokens[idx].attrJoin('class', 'raw');
     addPart(tokens, idx);
-    return self.renderToken.apply(self, arguments);
+    return self.renderToken(...arguments);
 };
-md.renderer.rules.fence = function (tokens, idx, options, env, self) {
-    var token = tokens[idx],
-      info = token.info ? md.utils.unescapeAll(token.info).trim() : '',
-      langName = '',
-      highlighted;
+md.renderer.rules.fence = (tokens, idx, options, env, self) => {
+    const token = tokens[idx];
+    const info = token.info ? md.utils.unescapeAll(token.info).trim() : '';
+    let langName = '';
+    let highlighted;
 
     if (info) {
         langName = info.split(/\s+/g)[0];
@@ -74,38 +75,33 @@ md.renderer.rules.fence = function (tokens, idx, options, env, self) {
     }
 
     if (highlighted.indexOf('<pre') === 0) {
-        return highlighted + '\n';
+        return `${highlighted}\n`;
     }
 
     if (tokens[idx].map && tokens[idx].level === 0) {
-        var startline = tokens[idx].map[0] + 1;
-        var endline = tokens[idx].map[1];
-        return '<pre class="part" data-startline="' + startline + '" data-endline="' + endline + '"><code' + self.renderAttrs(token) + '>'
-        + highlighted
-        + '</code></pre>\n';
+        const startline = tokens[idx].map[0] + 1;
+        const endline = tokens[idx].map[1];
+        return `<pre class="part" data-startline="${startline}" data-endline="${endline}"><code${self.renderAttrs(token)}>${highlighted}</code></pre>\n`;
     }
 
-    return '<pre><code' + self.renderAttrs(token) + '>'
-        + highlighted
-        + '</code></pre>\n';
+    return `<pre><code${self.renderAttrs(token)}>${highlighted}</code></pre>\n`;
 };
-md.renderer.rules.code_block = function (tokens, idx, options, env, self) {
+md.renderer.rules.code_block = (tokens, idx, options, env, self) => {
     if (tokens[idx].map && tokens[idx].level === 0) {
-        var startline = tokens[idx].map[0] + 1;
-        var endline = tokens[idx].map[1];
-        return '<pre class="part" data-startline="' + startline + '" data-endline="' + endline + '"><code>' + md.utils.escapeHtml(tokens[idx].content) + '</code></pre>\n';
+        const startline = tokens[idx].map[0] + 1;
+        const endline = tokens[idx].map[1];
+        return `<pre class="part" data-startline="${startline}" data-endline="${endline}"><code>${md.utils.escapeHtml(tokens[idx].content)}</code></pre>\n`;
     }
-    return '<pre><code>' + md.utils.escapeHtml(tokens[idx].content) + '</code></pre>\n';
+    return `<pre><code>${md.utils.escapeHtml(tokens[idx].content)}</code></pre>\n`;
 };
 function renderContainer(tokens, idx, options, env, self) {
     tokens[idx].attrJoin('role', 'alert');
     tokens[idx].attrJoin('class', 'alert');
-    tokens[idx].attrJoin('class', 'alert-' + tokens[idx].info.trim());
+    tokens[idx].attrJoin('class', `alert-${tokens[idx].info.trim()}`);
     addPart(tokens, idx);
-    return self.renderToken.apply(self, arguments);
+    return self.renderToken(...arguments);
 }
 
-var markdownitContainer = require('markdown-it-container');
 md.use(markdownitContainer, 'success', { render: renderContainer });
 md.use(markdownitContainer, 'info', { render: renderContainer });
 md.use(markdownitContainer, 'warning', { render: renderContainer });
@@ -117,18 +113,18 @@ window.syncscroll = true;
 window.preventSyncScrollToEdit = false;
 window.preventSyncScrollToView = false;
 
-var editScrollThrottle = 5;
-var viewScrollThrottle = 5;
-var buildMapThrottle = 100;
+const editScrollThrottle = 5;
+const viewScrollThrottle = 5;
+const buildMapThrottle = 100;
 
-var viewScrolling = false;
-var editScrolling = false;
+let viewScrolling = false;
+let editScrolling = false;
 
-var editArea = null;
-var viewArea = null;
-var markdownArea = null;
+let editArea = null;
+let viewArea = null;
+let markdownArea = null;
 
-function setupSyncAreas(edit, view, markdown) {
+export function setupSyncAreas(edit, view, markdown) {
     editArea = edit;
     viewArea = view;
     markdownArea = markdown;
@@ -136,26 +132,24 @@ function setupSyncAreas(edit, view, markdown) {
     viewArea.on('scroll', _.throttle(syncScrollToEdit, viewScrollThrottle));
 }
 
-var scrollMap, lineHeightMap, viewTop, viewBottom;
+let scrollMap, lineHeightMap, viewTop, viewBottom;
 
-window.viewAjaxCallback = clearMap;
-
-function clearMap() {
+export function clearMap() {
     scrollMap = null;
     lineHeightMap = null;
     viewTop = null;
     viewBottom = null;
 }
+window.viewAjaxCallback = clearMap;
 
-var buildMap = _.throttle(buildMapInner, buildMapThrottle);
+const buildMap = _.throttle(buildMapInner, buildMapThrottle);
 
 // Build offsets for each line (lines can be wrapped)
 // That's a bit dirty to process each line everytime, but ok for demo.
 // Optimizations are required only for big texts.
 function buildMapInner(callback) {
     if (!viewArea || !markdownArea) return;
-    var i, offset, nonEmptyList, pos, a, b, _lineHeightMap, linesCount,
-        acc, _scrollMap;
+    let i, offset, nonEmptyList, pos, a, b, _lineHeightMap, linesCount, acc, _scrollMap;
 
     offset = viewArea.scrollTop() - viewArea.offset().top;
     _scrollMap = [];
@@ -165,10 +159,10 @@ function buildMapInner(callback) {
     viewBottom = viewArea[0].scrollHeight - viewArea.height();
 
     acc = 0;
-    var lines = editor.getValue().split('\n');
-    var lineHeight = editor.defaultTextHeight();
+    const lines = editor.getValue().split('\n');
+    const lineHeight = editor.defaultTextHeight();
     for (i = 0; i < lines.length; i++) {
-        var str = lines[i];
+        const str = lines[i];
 
         _lineHeightMap.push(acc);
 
@@ -177,7 +171,7 @@ function buildMapInner(callback) {
             continue;
         }
 
-        var h = editor.heightAtLine(i + 1) - editor.heightAtLine(i);
+        const h = editor.heightAtLine(i + 1) - editor.heightAtLine(i);
         acc += Math.round(h / lineHeight);
     }
     _lineHeightMap.push(acc);
@@ -191,10 +185,10 @@ function buildMapInner(callback) {
     // make the first line go top
     _scrollMap[0] = viewTop;
 
-    var parts = markdownArea.find('.part').toArray();
+    const parts = markdownArea.find('.part').toArray();
     for (i = 0; i < parts.length; i++) {
-        var $el = $(parts[i]),
-            t = $el.attr('data-startline') - 1;
+        const $el = $(parts[i]);
+        let t = $el.attr('data-startline') - 1;
         if (t === '') {
             return;
         }
@@ -229,9 +223,9 @@ function buildMapInner(callback) {
 }
 
 // sync view scroll progress to edit
-var viewScrollingTimer = null;
+let viewScrollingTimer = null;
 
-function syncScrollToEdit(event, preventAnimate) {
+export function syncScrollToEdit(event, preventAnimate) {
     if (currentMode != modeType.both || !syncscroll || !editArea) return;
     if (preventSyncScrollToEdit) {
         if (typeof preventSyncScrollToEdit === 'number') {
@@ -242,15 +236,15 @@ function syncScrollToEdit(event, preventAnimate) {
         return;
     }
     if (!scrollMap || !lineHeightMap) {
-        buildMap(function () {
+        buildMap(() => {
             syncScrollToEdit(event, preventAnimate);
         });
         return;
     }
     if (editScrolling) return;
 
-    var scrollTop = viewArea[0].scrollTop;
-    var lineIndex = 0;
+    const scrollTop = viewArea[0].scrollTop;
+    let lineIndex = 0;
     for (var i = 0, l = scrollMap.length; i < l; i++) {
         if (scrollMap[i] > scrollTop) {
             break;
@@ -258,8 +252,8 @@ function syncScrollToEdit(event, preventAnimate) {
             lineIndex = i;
         }
     }
-    var lineNo = 0;
-    var lineDiff = 0;
+    let lineNo = 0;
+    let lineDiff = 0;
     for (var i = 0, l = lineHeightMap.length; i < l; i++) {
         if (lineHeightMap[i] > lineIndex) {
             break;
@@ -269,14 +263,14 @@ function syncScrollToEdit(event, preventAnimate) {
         }
     }
 
-    var posTo = 0;
-    var topDiffPercent = 0;
-    var posToNextDiff = 0;
-    var scrollInfo = editor.getScrollInfo();
-    var textHeight = editor.defaultTextHeight();
-    var preLastLineHeight = scrollInfo.height - scrollInfo.clientHeight - textHeight;
-    var preLastLineNo = Math.round(preLastLineHeight / textHeight);
-    var preLastLinePos = scrollMap[preLastLineNo];
+    let posTo = 0;
+    let topDiffPercent = 0;
+    let posToNextDiff = 0;
+    const scrollInfo = editor.getScrollInfo();
+    const textHeight = editor.defaultTextHeight();
+    const preLastLineHeight = scrollInfo.height - scrollInfo.clientHeight - textHeight;
+    const preLastLineNo = Math.round(preLastLineHeight / textHeight);
+    const preLastLinePos = scrollMap[preLastLineNo];
 
     if (scrollInfo.height > scrollInfo.clientHeight && scrollTop >= preLastLinePos) {
         posTo = preLastLineHeight;
@@ -293,7 +287,7 @@ function syncScrollToEdit(event, preventAnimate) {
     if (preventAnimate) {
         editArea.scrollTop(posTo);
     } else {
-        var posDiff = Math.abs(scrollInfo.top - posTo);
+        const posDiff = Math.abs(scrollInfo.top - posTo);
         var duration = posDiff / 50;
         duration = duration >= 100 ? duration : 100;
         editArea.stop(true, true).animate({
@@ -311,9 +305,9 @@ function viewScrollingTimeoutInner() {
 }
 
 // sync edit scroll progress to view
-var editScrollingTimer = null;
+let editScrollingTimer = null;
 
-function syncScrollToView(event, preventAnimate) {
+export function syncScrollToView(event, preventAnimate) {
     if (currentMode != modeType.both || !syncscroll || !viewArea) return;
     if (preventSyncScrollToView) {
         if (typeof preventSyncScrollToView === 'number') {
@@ -324,20 +318,20 @@ function syncScrollToView(event, preventAnimate) {
         return;
     }
     if (!scrollMap || !lineHeightMap) {
-        buildMap(function () {
+        buildMap(() => {
             syncScrollToView(event, preventAnimate);
         });
         return;
     }
     if (viewScrolling) return;
 
-    var lineNo, posTo;
-    var topDiffPercent, posToNextDiff;
-    var scrollInfo = editor.getScrollInfo();
-    var textHeight = editor.defaultTextHeight();
+    let lineNo, posTo;
+    let topDiffPercent, posToNextDiff;
+    const scrollInfo = editor.getScrollInfo();
+    const textHeight = editor.defaultTextHeight();
     lineNo = Math.floor(scrollInfo.top / textHeight);
     // if reach the last line, will start lerp to the bottom
-    var diffToBottom = (scrollInfo.top + scrollInfo.clientHeight) - (scrollInfo.height - textHeight);
+    const diffToBottom = (scrollInfo.top + scrollInfo.clientHeight) - (scrollInfo.height - textHeight);
     if (scrollInfo.height > scrollInfo.clientHeight && diffToBottom > 0) {
         topDiffPercent = diffToBottom / textHeight;
         posTo = scrollMap[lineNo + 1];
@@ -353,7 +347,7 @@ function syncScrollToView(event, preventAnimate) {
     if (preventAnimate) {
         viewArea.scrollTop(posTo);
     } else {
-        var posDiff = Math.abs(viewArea.scrollTop() - posTo);
+        const posDiff = Math.abs(viewArea.scrollTop() - posTo);
         var duration = posDiff / 50;
         duration = duration >= 100 ? duration : 100;
         viewArea.stop(true, true).animate({
@@ -369,10 +363,3 @@ function syncScrollToView(event, preventAnimate) {
 function editScrollingTimeoutInner() {
     editScrolling = false;
 }
-
-module.exports = {
-  setupSyncAreas: setupSyncAreas,
-  clearMap: clearMap,
-  syncScrollToEdit: syncScrollToEdit,
-  syncScrollToView: syncScrollToView
-};
diff --git a/public/screenshot.png b/public/screenshot.png
new file mode 100644
index 0000000..e1e7743
Binary files /dev/null and b/public/screenshot.png differ
diff --git a/public/vendor/ot/codemirror-adapter.js b/public/vendor/ot/codemirror-adapter.js
index d858c41..cc23be0 100755
--- a/public/vendor/ot/codemirror-adapter.js
+++ b/public/vendor/ot/codemirror-adapter.js
@@ -343,7 +343,9 @@ ot.CodeMirrorAdapter = (function (global) {
     };
 
     CodeMirrorAdapter.prototype.applyOperation = function (operation) {
-        this.ignoreNextChange = true;
+        if (!operation.isNoop()) {
+            this.ignoreNextChange = true;
+        }
         CodeMirrorAdapter.applyOperationToCodeMirror(operation, this.cm);
     };
 
diff --git a/public/vendor/ot/ot.min.js b/public/vendor/ot/ot.min.js
index aba0960..d942cb5 100644
--- a/public/vendor/ot/ot.min.js
+++ b/public/vendor/ot/ot.min.js
@@ -1 +1 @@
-function hex2rgb(t){if("#"==t[0]&&(t=t.substr(1)),3==t.length){var e=t;t="",e=/^([a-f0-9])([a-f0-9])([a-f0-9])$/i.exec(e).slice(1);for(var n=0;n<3;n++)t+=e[n]+e[n]}var o=/^([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(t).slice(1);return{red:parseInt(o[0],16),green:parseInt(o[1],16),blue:parseInt(o[2],16)}}if("undefined"==typeof ot)var ot={};if(ot.TextOperation=function(){"use strict";function t(){return this&&this.constructor===t?(this.ops=[],this.baseLength=0,void(this.targetLength=0)):new t}function e(e,n){var o=e.ops,r=t.isRetain;switch(o.length){case 1:return o[0];case 2:return r(o[0])?o[1]:r(o[1])?o[0]:null;case 3:if(r(o[0])&&r(o[2]))return o[1]}return null}function n(t){return o(t.ops[0])?t.ops[0]:0}t.prototype.equals=function(t){if(this.baseLength!==t.baseLength)return!1;if(this.targetLength!==t.targetLength)return!1;if(this.ops.length!==t.ops.length)return!1;for(var e=0;e<this.ops.length;e++)if(this.ops[e]!==t.ops[e])return!1;return!0};var o=t.isRetain=function(t){return"number"==typeof t&&t>0},r=t.isInsert=function(t){return"string"==typeof t},i=t.isDelete=function(t){return"number"==typeof t&&t<0};return t.prototype.retain=function(t){if("number"!=typeof t)throw new Error("retain expects an integer");return 0===t?this:(this.baseLength+=t,this.targetLength+=t,o(this.ops[this.ops.length-1])?this.ops[this.ops.length-1]+=t:this.ops.push(t),this)},t.prototype.insert=function(t){if("string"!=typeof t)throw new Error("insert expects a string");if(""===t)return this;this.targetLength+=t.length;var e=this.ops;return r(e[e.length-1])?e[e.length-1]+=t:i(e[e.length-1])?r(e[e.length-2])?e[e.length-2]+=t:(e[e.length]=e[e.length-1],e[e.length-2]=t):e.push(t),this},t.prototype["delete"]=function(t){if("string"==typeof t&&(t=t.length),"number"!=typeof t)throw new Error("delete expects an integer or a string");return 0===t?this:(t>0&&(t=-t),this.baseLength-=t,i(this.ops[this.ops.length-1])?this.ops[this.ops.length-1]+=t:this.ops.push(t),this)},t.prototype.isNoop=function(){return 0===this.ops.length||1===this.ops.length&&o(this.ops[0])},t.prototype.toString=function(){var t=Array.prototype.map||function(t){for(var e=this,n=[],o=0,r=e.length;o<r;o++)n[o]=t(e[o]);return n};return t.call(this.ops,function(t){return o(t)?"retain "+t:r(t)?"insert '"+t+"'":"delete "+-t}).join(", ")},t.prototype.toJSON=function(){return this.ops},t.fromJSON=function(e){for(var n=new t,s=0,a=e.length;s<a;s++){var h=e[s];if(o(h))n.retain(h);else if(r(h))n.insert(h);else{if(!i(h))throw new Error("unknown operation: "+JSON.stringify(h));n["delete"](h)}}return n},t.prototype.apply=function(t){var e=this;if(t.length!==e.baseLength)throw new Error("The operation's base length must be equal to the string's length.");for(var n=[],i=0,s=0,a=this.ops,h=0,p=a.length;h<p;h++){var c=a[h];if(o(c)){if(s+c>t.length)throw new Error("Operation can't retain more characters than are left in the string.");n[i++]=t.slice(s,s+c),s+=c}else r(c)?n[i++]=c:s-=c}if(s!==t.length)throw new Error("The operation didn't operate on the whole string.");return n.join("")},t.prototype.invert=function(e){for(var n=0,i=new t,s=this.ops,a=0,h=s.length;a<h;a++){var p=s[a];o(p)?(i.retain(p),n+=p):r(p)?i["delete"](p.length):(i.insert(e.slice(n,n-p)),n-=p)}return i},t.prototype.compose=function(e){var n=this;if(n.targetLength!==e.baseLength)throw new Error("The base length of the second operation has to be the target length of the first operation");for(var s=new t,a=n.ops,h=e.ops,p=0,c=0,l=a[p++],u=h[c++];;){if("undefined"==typeof l&&"undefined"==typeof u)break;if(i(l))s["delete"](l),l=a[p++];else if(r(u))s.insert(u),u=h[c++];else{if("undefined"==typeof l)throw new Error("Cannot compose operations: first operation is too short.");if("undefined"==typeof u)throw new Error("Cannot compose operations: first operation is too long.");if(o(l)&&o(u))l>u?(s.retain(u),l-=u,u=h[c++]):l===u?(s.retain(l),l=a[p++],u=h[c++]):(s.retain(l),u-=l,l=a[p++]);else if(r(l)&&i(u))l.length>-u?(l=l.slice(-u),u=h[c++]):l.length===-u?(l=a[p++],u=h[c++]):(u+=l.length,l=a[p++]);else if(r(l)&&o(u))l.length>u?(s.insert(l.slice(0,u)),l=l.slice(u),u=h[c++]):l.length===u?(s.insert(l),l=a[p++],u=h[c++]):(s.insert(l),u-=l.length,l=a[p++]);else{if(!o(l)||!i(u))throw new Error("This shouldn't happen: op1: "+JSON.stringify(l)+", op2: "+JSON.stringify(u));l>-u?(s["delete"](u),l+=u,u=h[c++]):l===-u?(s["delete"](u),l=a[p++],u=h[c++]):(s["delete"](l),u+=l,l=a[p++])}}}return s},t.prototype.shouldBeComposedWith=function(t){if(this.isNoop()||t.isNoop())return!0;var o=n(this),s=n(t),a=e(this),h=e(t);return!(!a||!h)&&(r(a)&&r(h)?o+a.length===s:!(!i(a)||!i(h))&&(s-h===o||o===s))},t.prototype.shouldBeComposedWithInverted=function(t){if(this.isNoop()||t.isNoop())return!0;var o=n(this),s=n(t),a=e(this),h=e(t);return!(!a||!h)&&(r(a)&&r(h)?o+a.length===s||o===s:!(!i(a)||!i(h))&&s-h===o)},t.transform=function(e,n){if(e.baseLength!==n.baseLength)throw new Error("Both operations have to have the same base length");for(var s=new t,a=new t,h=e.ops,p=n.ops,c=0,l=0,u=h[c++],f=p[l++];;){if("undefined"==typeof u&&"undefined"==typeof f)break;if(r(u))s.insert(u),a.retain(u.length),u=h[c++];else if(r(f))s.retain(f.length),a.insert(f),f=p[l++];else{if("undefined"==typeof u)throw new Error("Cannot compose operations: first operation is too short.");if("undefined"==typeof f)throw new Error("Cannot compose operations: first operation is too long.");var d;if(o(u)&&o(f))u>f?(d=f,u-=f,f=p[l++]):u===f?(d=f,u=h[c++],f=p[l++]):(d=u,f-=u,u=h[c++]),s.retain(d),a.retain(d);else if(i(u)&&i(f))-u>-f?(u-=f,f=p[l++]):u===f?(u=h[c++],f=p[l++]):(f-=u,u=h[c++]);else if(i(u)&&o(f))-u>f?(d=f,u+=f,f=p[l++]):-u===f?(d=f,u=h[c++],f=p[l++]):(d=-u,f+=u,u=h[c++]),s["delete"](d);else{if(!o(u)||!i(f))throw new Error("The two operations aren't compatible");u>-f?(d=-f,u+=f,f=p[l++]):u===-f?(d=u,u=h[c++],f=p[l++]):(d=u,f+=u,u=h[c++]),a["delete"](d)}}}return[s,a]},t}(),"object"==typeof module&&(module.exports=ot.TextOperation),"undefined"==typeof ot)var ot={};if(ot.Selection=function(t){"use strict";function e(t,e){this.anchor=t,this.head=e}function n(t){this.ranges=t||[]}var o=t.ot?t.ot.TextOperation:require("./text-operation");return e.fromJSON=function(t){return new e(t.anchor,t.head)},e.prototype.equals=function(t){return this.anchor===t.anchor&&this.head===t.head},e.prototype.isEmpty=function(){return this.anchor===this.head},e.prototype.transform=function(t){function n(e){for(var n=e,r=t.ops,i=0,s=t.ops.length;i<s&&(o.isRetain(r[i])?e-=r[i]:o.isInsert(r[i])?n+=r[i].length:(n-=Math.min(e,-r[i]),e+=r[i]),!(e<0));i++);return n}var r=n(this.anchor);return this.anchor===this.head?new e(r,r):new e(r,n(this.head))},n.Range=e,n.createCursor=function(t){return new n([new e(t,t)])},n.fromJSON=function(t){for(var o=t.ranges||t,r=0,i=[];r<o.length;r++)i[r]=e.fromJSON(o[r]);return new n(i)},n.prototype.equals=function(t){if(this.position!==t.position)return!1;if(this.ranges.length!==t.ranges.length)return!1;for(var e=0;e<this.ranges.length;e++)if(!this.ranges[e].equals(t.ranges[e]))return!1;return!0},n.prototype.somethingSelected=function(){for(var t=0;t<this.ranges.length;t++)if(!this.ranges[t].isEmpty())return!0;return!1},n.prototype.compose=function(t){return t},n.prototype.transform=function(t){for(var e=0,o=[];e<this.ranges.length;e++)o[e]=this.ranges[e].transform(t);return new n(o)},n}(this),"object"==typeof module&&(module.exports=ot.Selection),"undefined"==typeof ot)var ot={};if(ot.WrappedOperation=function(t){"use strict";function e(t,e){this.wrapped=t,this.meta=e}function n(t,e){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])}function o(t,e){if(t&&"object"==typeof t){if("function"==typeof t.compose)return t.compose(e);var o={};return n(t,o),n(e,o),o}return e}function r(t,e){return t&&"object"==typeof t&&"function"==typeof t.transform?t.transform(e):t}return e.prototype.apply=function(){return this.wrapped.apply.apply(this.wrapped,arguments)},e.prototype.invert=function(){var t=this.meta;return new e(this.wrapped.invert.apply(this.wrapped,arguments),t&&"object"==typeof t&&"function"==typeof t.invert?t.invert.apply(t,arguments):t)},e.prototype.compose=function(t){return new e(this.wrapped.compose(t.wrapped),o(this.meta,t.meta))},e.transform=function(t,n){var o=t.wrapped.constructor.transform,i=o(t.wrapped,n.wrapped);return[new e(i[0],r(t.meta,n.wrapped)),new e(i[1],r(n.meta,t.wrapped))]},e}(this),"object"==typeof module&&(module.exports=ot.WrappedOperation),"undefined"==typeof ot)var ot={};if(ot.UndoManager=function(){"use strict";function t(t){this.maxItems=t||50,this.state=n,this.dontCompose=!1,this.undoStack=[],this.redoStack=[]}function e(t,e){for(var n=[],o=e.constructor,r=t.length-1;r>=0;r--){var i=o.transform(t[r],e);"function"==typeof i[0].isNoop&&i[0].isNoop()||n.push(i[0]),e=i[1]}return n.reverse()}var n="normal",o="undoing",r="redoing";return t.prototype.add=function(t,e){if(this.state===o)this.redoStack.push(t),this.dontCompose=!0;else if(this.state===r)this.undoStack.push(t),this.dontCompose=!0;else{var n=this.undoStack;!this.dontCompose&&e&&n.length>0?n.push(t.compose(n.pop())):(n.push(t),n.length>this.maxItems&&n.shift()),this.dontCompose=!1,this.redoStack=[]}},t.prototype.transform=function(t){this.undoStack=e(this.undoStack,t),this.redoStack=e(this.redoStack,t)},t.prototype.performUndo=function(t){if(this.state=o,0===this.undoStack.length)throw new Error("undo not possible");t(this.undoStack.pop()),this.state=n},t.prototype.performRedo=function(t){if(this.state=r,0===this.redoStack.length)throw new Error("redo not possible");t(this.redoStack.pop()),this.state=n},t.prototype.canUndo=function(){return 0!==this.undoStack.length},t.prototype.canRedo=function(){return 0!==this.redoStack.length},t.prototype.isUndoing=function(){return this.state===o},t.prototype.isRedoing=function(){return this.state===r},t}(),"object"==typeof module&&(module.exports=ot.UndoManager),"undefined"==typeof ot)var ot={};ot.Client=function(t){"use strict";function e(t){this.revision=t,this.setState(a)}function n(){}function o(t){this.outstanding=t}function r(t,e){this.outstanding=t,this.buffer=e}function i(t,e,n){this.acknowlaged=t,this.client=e,this.revision=n}function s(t,e,n,o){this.acknowlaged=t,this.buffer=e,this.client=n,this.revision=o}e.prototype.setState=function(t){this.state=t},e.prototype.applyClient=function(t){this.setState(this.state.applyClient(this,t))},e.prototype.applyServer=function(t,e){this.setState(this.state.applyServer(this,t,e))},e.prototype.applyOperations=function(t,e){this.setState(this.state.applyOperations(this,t,e))},e.prototype.serverAck=function(t){this.setState(this.state.serverAck(this,t))},e.prototype.serverReconnect=function(){"function"==typeof this.state.resend&&this.state.resend(this)},e.prototype.transformSelection=function(t){return this.state.transformSelection(t)},e.prototype.sendOperation=function(t,e){throw new Error("sendOperation must be defined in child class")},e.prototype.applyOperation=function(t){throw new Error("applyOperation must be defined in child class")},e.Synchronized=n,n.prototype.applyClient=function(t,e){return t.sendOperation(t.revision,e),new o(e)},n.prototype.applyServer=function(t,e,n){if(e-t.revision>1)throw new Error("Invalid revision.");return t.revision=e,t.applyOperation(n),this},n.prototype.serverAck=function(t,e){throw new Error("There is no pending operation.")},n.prototype.transformSelection=function(t){return t};var a=new n;return e.AwaitingConfirm=o,o.prototype.applyClient=function(t,e){return new r(this.outstanding,e)},o.prototype.applyServer=function(t,e,n){if(e-t.revision>1)throw new Error("Invalid revision.");t.revision=e;var r=n.constructor.transform(this.outstanding,n);return t.applyOperation(r[1]),new o(r[0])},o.prototype.serverAck=function(t,e){return e-t.revision>1?new i(this.outstanding,t,e).getOperations():(t.revision=e,a)},o.prototype.transformSelection=function(t){return t.transform(this.outstanding)},o.prototype.resend=function(t){t.sendOperation(t.revision,this.outstanding)},e.AwaitingWithBuffer=r,r.prototype.applyClient=function(t,e){var n=this.buffer.compose(e);return new r(this.outstanding,n)},r.prototype.applyServer=function(t,e,n){if(e-t.revision>1)throw new Error("Invalid revision.");t.revision=e;var o=n.constructor.transform,i=o(this.outstanding,n),s=o(this.buffer,i[1]);return t.applyOperation(s[1]),new r(i[0],s[0])},r.prototype.serverAck=function(t,e){return e-t.revision>1?new s(this.outstanding,this.buffer,t,e).getOperations():(t.revision=e,t.sendOperation(t.revision,this.buffer),new o(this.buffer))},r.prototype.transformSelection=function(t){return t.transform(this.outstanding).transform(this.buffer)},r.prototype.resend=function(t){t.sendOperation(t.revision,this.outstanding)},e.Stale=i,i.prototype.applyClient=function(t,e){return new s(this.acknowlaged,e,t,this.revision)},i.prototype.applyServer=function(t,e,n){throw new Error("Ignored server-side change.")},i.prototype.applyOperations=function(t,e,n){for(var o=this.acknowlaged.constructor.transform,r=0;r<n.length;r++){var i=ot.TextOperation.fromJSON(n[r]),s=o(this.acknowlaged,i);t.applyOperation(s[1]),this.acknowlaged=s[0]}return t.revision=this.revision,a},i.prototype.serverAck=function(t,e){throw new Error("There is no pending operation.")},i.prototype.transformSelection=function(t){return t},i.prototype.getOperations=function(){return this.client.getOperations(this.client.revision,this.revision-1),this},e.StaleWithBuffer=s,s.prototype.applyClient=function(t,e){var n=this.buffer.compose(e);return new s(this.acknowlaged,n,t,this.revision)},s.prototype.applyServer=function(t,e,n){throw new Error("Ignored server-side change.")},s.prototype.applyOperations=function(t,e,n){for(var r=this.acknowlaged.constructor.transform,i=0;i<n.length;i++){var s=ot.TextOperation.fromJSON(n[i]),a=r(this.acknowlaged,s),h=r(this.buffer,a[1]);t.applyOperation(h[1]),this.acknowlaged=a[0],this.buffer=h[0]}return t.revision=this.revision,t.sendOperation(t.revision,this.buffer),new o(this.buffer)},s.prototype.serverAck=function(t,e){throw new Error("There is no pending operation.")},s.prototype.transformSelection=function(t){return t},s.prototype.getOperations=function(){return this.client.getOperations(this.client.revision,this.revision-1),this},e}(this),"object"==typeof module&&(module.exports=ot.Client),ot.CodeMirrorAdapter=function(t){"use strict";function e(t){this.cm=t,this.ignoreNextChange=!1,this.changeInProgress=!1,this.selectionChanged=!1,a(this,"onChanges"),a(this,"onChange"),a(this,"onCursorActivity"),a(this,"onFocus"),a(this,"onBlur"),t.on("changes",this.onChanges),t.on("change",this.onChange),t.on("cursorActivity",this.onCursorActivity),t.on("focus",this.onFocus),t.on("blur",this.onBlur)}function n(t,e){return t.line<e.line?-1:t.line>e.line?1:t.ch<e.ch?-1:t.ch>e.ch?1:0}function o(t,e){return n(t,e)<=0}function r(t,e){return o(t,e)?t:e}function i(t,e){return o(t,e)?e:t}function s(t){return t.indexFromPos({line:t.lastLine(),ch:0})+t.getLine(t.lastLine()).length}function a(t,e){var n=t[e];t[e]=function(){n.apply(t,arguments)}}var h=ot.TextOperation,p=ot.Selection;e.prototype.detach=function(){this.cm.off("changes",this.onChanges),this.cm.off("change",this.onChange),this.cm.off("cursorActivity",this.onCursorActivity),this.cm.off("focus",this.onFocus),this.cm.off("blur",this.onBlur)},e.operationFromCodeMirrorChanges=function(t,e){function n(t){return t[t.length-1]}function r(t){if(0===t.length)return 0;for(var e=0,n=0;n<t.length;n++)e+=t[n].length;return e+t.length-1}function i(t,e){return function(i){return o(i,e.from)?t(i):o(e.to,i)?t({line:i.line+e.text.length-1-(e.to.line-e.from.line),ch:e.to.line<i.line?i.ch:e.text.length<=1?i.ch-(e.to.ch-e.from.ch)+r(e.text):i.ch-e.to.ch+n(e.text).length})+r(e.removed)-r(e.text):e.from.line===i.line?t(e.from)+i.ch-e.from.ch:t(e.from)+r(e.removed.slice(0,i.line-e.from.line))+1+i.ch}}for(var a=s(e),p=(new h).retain(a),c=(new h).retain(a),l=function(t){return e.indexFromPos(t)},u=t.length-1;u>=0;u--){var f=t[u];l=i(l,f);var d=l(f.from),g=a-d-r(f.text);p=(new h).retain(d)["delete"](r(f.removed)).insert(f.text.join("\n")).retain(g).compose(p),c=c.compose((new h).retain(d)["delete"](r(f.text)).insert(f.removed.join("\n")).retain(g)),a+=r(f.removed)-r(f.text)}return[p,c]},e.operationFromCodeMirrorChange=e.operationFromCodeMirrorChanges,e.applyOperationToCodeMirror=function(t,e){e.operation(function(){for(var n=t.ops,o=0,r=0,i=n.length;r<i;r++){var s=n[r];if(h.isRetain(s))o+=s;else if(h.isInsert(s))e.replaceRange(s,e.posFromIndex(o),null,"ignoreHistory"),o+=s.length;else if(h.isDelete(s)){var a=e.posFromIndex(o),p=e.posFromIndex(o-s);e.replaceRange("",a,p,"ignoreHistory")}}})},e.prototype.registerCallbacks=function(t){this.callbacks=t},e.prototype.onChange=function(){this.changeInProgress=!0},e.prototype.onChanges=function(t,n){if(!this.ignoreNextChange){var o=e.operationFromCodeMirrorChanges(n,this.cm);this.trigger("change",o[0],o[1])}this.selectionChanged&&this.trigger("selectionChange"),this.changeInProgress=!1,this.ignoreNextChange=!1},e.prototype.onCursorActivity=e.prototype.onFocus=function(){this.changeInProgress?this.selectionChanged=!0:this.trigger("selectionChange")},e.prototype.onBlur=function(){this.cm.somethingSelected()||this.trigger("blur")},e.prototype.getValue=function(){return this.cm.getValue()},e.prototype.getSelection=function(){for(var t=this.cm,e=t.listSelections(),n=[],o=0;o<e.length;o++)n[o]=new p.Range(t.indexFromPos(e[o].anchor),t.indexFromPos(e[o].head));return new p(n)},e.prototype.setSelection=function(t){for(var e=[],n=0;t&&n<t.ranges.length;n++){var o=t.ranges[n];e[n]={anchor:this.cm.posFromIndex(o.anchor),head:this.cm.posFromIndex(o.head)}}this.cm.setSelections(e)};var c=function(){var t={},e=document.createElement("style");document.documentElement.getElementsByTagName("head")[0].appendChild(e);var n=e.sheet;return function(e){t[e]||(t[e]=!0,n.insertRule(e,(n.cssRules||n.rules).length))}}();return e.prototype.setOtherCursor=function(t,e,n){var o=this.cm.posFromIndex(t),r=(this.cm.cursorCoords(o),document.createElement("span"));return r.className="other-client",r.style.display="none",r.setAttribute("data-clientid",n),this.cm.setBookmark(o,{widget:r,insertLeft:!0})},e.prototype.setOtherSelectionRange=function(t,e,n){var o=/^#([0-9a-fA-F]{6})$/.exec(e);if(!o)throw new Error("only six-digit hex colors are allowed.");var s="selection-"+o[1],a=hex2rgb(e),h="."+s+" { background: rgba("+a.red+","+a.green+","+a.blue+",0.2); }";c(h);var p=this.cm.posFromIndex(t.anchor),l=this.cm.posFromIndex(t.head);return this.cm.markText(r(p,l),i(p,l),{className:s})},e.prototype.setOtherSelection=function(t,e,n){for(var o=[],r=0;r<t.ranges.length;r++){var i=t.ranges[r];i.isEmpty()||(o[r]=this.setOtherSelectionRange(i,e,n))}return{clear:function(){for(var t=0;t<o.length;t++)o[t]&&o[t].clear()}}},e.prototype.trigger=function(t){var e=Array.prototype.slice.call(arguments,1),n=this.callbacks&&this.callbacks[t];n&&n.apply(this,e)},e.prototype.applyOperation=function(t){this.ignoreNextChange=!0,e.applyOperationToCodeMirror(t,this.cm)},e.prototype.registerUndo=function(t){this.cm.undo=t},e.prototype.registerRedo=function(t){this.cm.redo=t},e}(this),ot.SocketIOAdapter=function(){"use strict";function t(t){this.socket=t;var e=this;t.on("client_left",function(t){e.trigger("client_left",t)}),t.on("set_name",function(t,n){e.trigger("set_name",t,n)}),t.on("set_color",function(t,n){e.trigger("set_color",t,n)}),t.on("ack",function(t){e.trigger("ack",t)}),t.on("operation",function(t,n,o,r){e.trigger("operation",n,o),e.trigger("selection",t,r)}),t.on("operations",function(t,n){e.trigger("operations",t,n)}),t.on("selection",function(t,n){e.trigger("selection",t,n)}),t.on("reconnect",function(){e.trigger("reconnect")})}return t.prototype.sendOperation=function(t,e,n){this.socket.emit("operation",t,e,n)},t.prototype.sendSelection=function(t){this.socket.emit("selection",t)},t.prototype.getOperations=function(t,e){this.socket.emit("get_operations",t,e)},t.prototype.registerCallbacks=function(t){this.callbacks=t},t.prototype.trigger=function(t){var e=Array.prototype.slice.call(arguments,1),n=this.callbacks&&this.callbacks[t];n&&n.apply(this,e)},t}(),ot.AjaxAdapter=function(){"use strict";function t(t,e,n){"/"!==t[t.length-1]&&(t+="/"),this.path=t,this.ownUserName=e,this.majorRevision=n.major||0,this.minorRevision=n.minor||0,this.poll()}return t.prototype.renderRevisionPath=function(){return"revision/"+this.majorRevision+"-"+this.minorRevision},t.prototype.handleResponse=function(t){var e,n=t.operations;for(e=0;e<n.length;e++)n[e].user===this.ownUserName?this.trigger("ack"):this.trigger("operation",n[e].operation);n.length>0&&(this.majorRevision+=n.length,this.minorRevision=0);var o=t.events;if(o){for(e=0;e<o.length;e++){var r=o[e].user;if(r!==this.ownUserName)switch(o[e].event){case"joined":this.trigger("set_name",r,r);break;case"left":this.trigger("client_left",r);break;case"selection":this.trigger("selection",r,o[e].selection)}}this.minorRevision+=o.length}var i=t.users;i&&(delete i[this.ownUserName],this.trigger("clients",i)),t.revision&&(this.majorRevision=t.revision.major,this.minorRevision=t.revision.minor)},t.prototype.poll=function(){var t=this;$.ajax({url:this.path+this.renderRevisionPath(),type:"GET",dataType:"json",timeout:5e3,success:function(e){t.handleResponse(e),t.poll()},error:function(){setTimeout(function(){t.poll()},500)}})},t.prototype.sendOperation=function(t,e,n){if(t!==this.majorRevision)throw new Error("Revision numbers out of sync");var o=this;$.ajax({url:this.path+this.renderRevisionPath(),type:"POST",data:JSON.stringify({operation:e,selection:n}),contentType:"application/json",processData:!1,success:function(t){},error:function(){setTimeout(function(){o.sendOperation(t,e,n)},500)}})},t.prototype.sendSelection=function(t){$.ajax({url:this.path+this.renderRevisionPath()+"/selection",type:"POST",data:JSON.stringify(t),contentType:"application/json",processData:!1,timeout:1e3})},t.prototype.registerCallbacks=function(t){this.callbacks=t},t.prototype.trigger=function(t){var e=Array.prototype.slice.call(arguments,1),n=this.callbacks&&this.callbacks[t];n&&n.apply(this,e)},t}(),ot.EditorClient=function(){"use strict";function t(t,e){this.selectionBefore=t,this.selectionAfter=e}function e(t,e){this.clientId=t,this.selection=e}function n(t,e,n,o,r,i){this.id=t,this.listEl=e,this.editorAdapter=n,this.name=o,this.color=r,this.li=document.createElement("li"),o&&(this.li.textContent=o,this.listEl.appendChild(this.li)),r?this.setForceColor(r):this.setColor(o?s(o):Math.random()),i&&this.updateSelection(i)}function o(t,e,n,o){c.call(this,t),this.serverAdapter=n,this.editorAdapter=o,this.undoManager=new u,this.initializeClientList(),this.initializeClients(e);var r=this;this.editorAdapter.registerCallbacks({change:function(t,e){r.onChange(t,e)},selectionChange:function(){r.onSelectionChange()},blur:function(){r.onBlur()}}),this.editorAdapter.registerUndo(function(){r.undo()}),this.editorAdapter.registerRedo(function(){r.redo()}),this.serverAdapter.registerCallbacks({client_left:function(t){r.onClientLeft(t)},set_name:function(t,e){r.getClientObject(t).setName(e)},set_color:function(t,e){r.getClientObject(t).setForceColor(e)},ack:function(t){r.serverAck(t)},operation:function(t,e){r.applyServer(t,f.fromJSON(e))},operations:function(t,e){r.applyOperations(t,e)},selection:function(t,e){e?r.getClientObject(t).updateSelection(r.transformSelection(l.fromJSON(e))):r.getClientObject(t).removeSelection()},clients:function(t){var e;for(e in r.clients)r.clients.hasOwnProperty(e)&&!t.hasOwnProperty(e)&&r.onClientLeft(e);for(e in t)if(t.hasOwnProperty(e)){var n=r.getClientObject(e);t[e].name&&n.setName(t[e].name);var o=t[e].selection;o?r.clients[e].updateSelection(r.transformSelection(l.fromJSON(o))):r.clients[e].removeSelection()}},reconnect:function(){r.serverReconnect()}})}function r(t,e,n){function o(t){var e=Math.round(255*t).toString(16);return 1===e.length?"0"+e:e}return"#"+o(t)+o(e)+o(n)}function i(t,e,n){if(0===e)return r(n,n,n);var o=n<.5?n*(1+e):n+e-e*n,i=2*n-o,s=function(t){return t<0&&(t+=1),t>1&&(t-=1),6*t<1?i+6*(o-i)*t:2*t<1?o:3*t<2?i+6*(o-i)*(2/3-t):i};return r(s(t+1/3),s(t),s(t-1/3))}function s(t){for(var e=1,n=0;n<t.length;n++)e=17*(e+t.charCodeAt(n))%360;return e/360}function a(t,e){function n(){}n.prototype=e.prototype,t.prototype=new n,t.prototype.constructor=t}function h(t){return t[t.length-1]}function p(t){t.parentNode&&t.parentNode.removeChild(t)}var c=ot.Client,l=ot.Selection,u=ot.UndoManager,f=ot.TextOperation,d=ot.WrappedOperation;return t.prototype.invert=function(){return new t(this.selectionAfter,this.selectionBefore)},t.prototype.compose=function(e){return new t(this.selectionBefore,e.selectionAfter)},t.prototype.transform=function(e){return new t(this.selectionBefore?this.selectionBefore.transform(e):null,this.selectionAfter?this.selectionAfter.transform(e):null)},e.fromJSON=function(t){return new e(t.clientId,t.selection&&l.fromJSON(t.selection))},e.prototype.transform=function(t){return new e(this.clientId,this.selection&&this.selection.transform(t))},n.prototype.setColor=function(t){this.hue=t,this.color=i(t,.75,.5),this.lightColor=i(t,.5,.9),this.li&&(this.li.style.color=this.color)},n.prototype.setForceColor=function(t){this.hue=null,this.color=t,this.lightColor=t,this.li&&(this.li.style.color=this.color)},n.prototype.setName=function(t){this.name!==t&&(this.name=t,this.li.textContent=t,this.li.parentNode||this.listEl.appendChild(this.li),this.setColor(s(t)))},n.prototype.updateSelection=function(t){this.removeSelection(),this.selection=t,this.mark=this.editorAdapter.setOtherSelection(t,t.position===t.selectionEnd?this.color:this.lightColor,this.id)},n.prototype.remove=function(){this.li&&p(this.li),this.removeSelection()},n.prototype.removeSelection=function(){this.mark&&(this.mark.clear(),this.mark=null)},a(o,c),o.prototype.addClient=function(t,e){this.clients[t]=new n(t,this.clientListEl,this.editorAdapter,e.name||t,e.color||null,e.selection?l.fromJSON(e.selection):null)},o.prototype.initializeClients=function(t){this.clients={};for(var e in t)t.hasOwnProperty(e)&&this.addClient(e,t[e])},o.prototype.getClientObject=function(t){var e=this.clients[t];return e?e:this.clients[t]=new n(t,this.clientListEl,this.editorAdapter)},o.prototype.onClientLeft=function(t){var e=this.clients[t];e&&(e.remove(),delete this.clients[t])},o.prototype.initializeClientList=function(){this.clientListEl=document.createElement("ul")},o.prototype.applyUnredo=function(t){this.undoManager.add(t.invert(this.editorAdapter.getValue())),this.editorAdapter.applyOperation(t.wrapped),this.selection=t.meta.selectionAfter,this.editorAdapter.setSelection(this.selection),this.applyClient(t.wrapped)},o.prototype.undo=function(){var t=this;this.undoManager.canUndo()&&this.undoManager.performUndo(function(e){t.applyUnredo(e)})},o.prototype.redo=function(){var t=this;this.undoManager.canRedo()&&this.undoManager.performRedo(function(e){t.applyUnredo(e)})},o.prototype.onChange=function(e,n){var o=this.selection;this.updateSelection();var r=new t(o,this.selection),i=(new d(e,r),this.undoManager.undoStack.length>0&&n.shouldBeComposedWithInverted(h(this.undoManager.undoStack).wrapped)),s=new t(this.selection,o);this.undoManager.add(new d(n,s),i),this.applyClient(e)},o.prototype.updateSelection=function(){this.selection=this.editorAdapter.getSelection()},o.prototype.onSelectionChange=function(){var t=this.selection;this.updateSelection(),t&&this.selection.equals(t)||this.sendSelection(this.selection)},o.prototype.onBlur=function(){this.selection=null,this.sendSelection(null)},o.prototype.sendSelection=function(t){this.state instanceof c.AwaitingWithBuffer||this.serverAdapter.sendSelection(t)},o.prototype.sendOperation=function(t,e){this.serverAdapter.sendOperation(t,e.toJSON(),this.selection)},o.prototype.getOperations=function(t,e){this.serverAdapter.getOperations(t,e)},o.prototype.applyOperation=function(t){this.editorAdapter.applyOperation(t),this.updateSelection(),this.undoManager.transform(new d(t,null))},o}();
\ No newline at end of file
+function hex2rgb(t){if("#"==t[0]&&(t=t.substr(1)),3==t.length){var e=t;t="",e=/^([a-f0-9])([a-f0-9])([a-f0-9])$/i.exec(e).slice(1);for(var n=0;n<3;n++)t+=e[n]+e[n]}var o=/^([a-f0-9]{2})([a-f0-9]{2})([a-f0-9]{2})$/i.exec(t).slice(1);return{red:parseInt(o[0],16),green:parseInt(o[1],16),blue:parseInt(o[2],16)}}if("undefined"==typeof ot)var ot={};if(ot.TextOperation=function(){"use strict";function t(){return this&&this.constructor===t?(this.ops=[],this.baseLength=0,void(this.targetLength=0)):new t}function e(e,n){var o=e.ops,r=t.isRetain;switch(o.length){case 1:return o[0];case 2:return r(o[0])?o[1]:r(o[1])?o[0]:null;case 3:if(r(o[0])&&r(o[2]))return o[1]}return null}function n(t){return o(t.ops[0])?t.ops[0]:0}t.prototype.equals=function(t){if(this.baseLength!==t.baseLength)return!1;if(this.targetLength!==t.targetLength)return!1;if(this.ops.length!==t.ops.length)return!1;for(var e=0;e<this.ops.length;e++)if(this.ops[e]!==t.ops[e])return!1;return!0};var o=t.isRetain=function(t){return"number"==typeof t&&t>0},r=t.isInsert=function(t){return"string"==typeof t},i=t.isDelete=function(t){return"number"==typeof t&&t<0};return t.prototype.retain=function(t){if("number"!=typeof t)throw new Error("retain expects an integer");return 0===t?this:(this.baseLength+=t,this.targetLength+=t,o(this.ops[this.ops.length-1])?this.ops[this.ops.length-1]+=t:this.ops.push(t),this)},t.prototype.insert=function(t){if("string"!=typeof t)throw new Error("insert expects a string");if(""===t)return this;this.targetLength+=t.length;var e=this.ops;return r(e[e.length-1])?e[e.length-1]+=t:i(e[e.length-1])?r(e[e.length-2])?e[e.length-2]+=t:(e[e.length]=e[e.length-1],e[e.length-2]=t):e.push(t),this},t.prototype["delete"]=function(t){if("string"==typeof t&&(t=t.length),"number"!=typeof t)throw new Error("delete expects an integer or a string");return 0===t?this:(t>0&&(t=-t),this.baseLength-=t,i(this.ops[this.ops.length-1])?this.ops[this.ops.length-1]+=t:this.ops.push(t),this)},t.prototype.isNoop=function(){return 0===this.ops.length||1===this.ops.length&&o(this.ops[0])},t.prototype.toString=function(){var t=Array.prototype.map||function(t){for(var e=this,n=[],o=0,r=e.length;o<r;o++)n[o]=t(e[o]);return n};return t.call(this.ops,function(t){return o(t)?"retain "+t:r(t)?"insert '"+t+"'":"delete "+-t}).join(", ")},t.prototype.toJSON=function(){return this.ops},t.fromJSON=function(e){for(var n=new t,s=0,a=e.length;s<a;s++){var h=e[s];if(o(h))n.retain(h);else if(r(h))n.insert(h);else{if(!i(h))throw new Error("unknown operation: "+JSON.stringify(h));n["delete"](h)}}return n},t.prototype.apply=function(t){var e=this;if(t.length!==e.baseLength)throw new Error("The operation's base length must be equal to the string's length.");for(var n=[],i=0,s=0,a=this.ops,h=0,p=a.length;h<p;h++){var c=a[h];if(o(c)){if(s+c>t.length)throw new Error("Operation can't retain more characters than are left in the string.");n[i++]=t.slice(s,s+c),s+=c}else r(c)?n[i++]=c:s-=c}if(s!==t.length)throw new Error("The operation didn't operate on the whole string.");return n.join("")},t.prototype.invert=function(e){for(var n=0,i=new t,s=this.ops,a=0,h=s.length;a<h;a++){var p=s[a];o(p)?(i.retain(p),n+=p):r(p)?i["delete"](p.length):(i.insert(e.slice(n,n-p)),n-=p)}return i},t.prototype.compose=function(e){var n=this;if(n.targetLength!==e.baseLength)throw new Error("The base length of the second operation has to be the target length of the first operation");for(var s=new t,a=n.ops,h=e.ops,p=0,c=0,l=a[p++],u=h[c++];;){if("undefined"==typeof l&&"undefined"==typeof u)break;if(i(l))s["delete"](l),l=a[p++];else if(r(u))s.insert(u),u=h[c++];else{if("undefined"==typeof l)throw new Error("Cannot compose operations: first operation is too short.");if("undefined"==typeof u)throw new Error("Cannot compose operations: first operation is too long.");if(o(l)&&o(u))l>u?(s.retain(u),l-=u,u=h[c++]):l===u?(s.retain(l),l=a[p++],u=h[c++]):(s.retain(l),u-=l,l=a[p++]);else if(r(l)&&i(u))l.length>-u?(l=l.slice(-u),u=h[c++]):l.length===-u?(l=a[p++],u=h[c++]):(u+=l.length,l=a[p++]);else if(r(l)&&o(u))l.length>u?(s.insert(l.slice(0,u)),l=l.slice(u),u=h[c++]):l.length===u?(s.insert(l),l=a[p++],u=h[c++]):(s.insert(l),u-=l.length,l=a[p++]);else{if(!o(l)||!i(u))throw new Error("This shouldn't happen: op1: "+JSON.stringify(l)+", op2: "+JSON.stringify(u));l>-u?(s["delete"](u),l+=u,u=h[c++]):l===-u?(s["delete"](u),l=a[p++],u=h[c++]):(s["delete"](l),u+=l,l=a[p++])}}}return s},t.prototype.shouldBeComposedWith=function(t){if(this.isNoop()||t.isNoop())return!0;var o=n(this),s=n(t),a=e(this),h=e(t);return!(!a||!h)&&(r(a)&&r(h)?o+a.length===s:!(!i(a)||!i(h))&&(s-h===o||o===s))},t.prototype.shouldBeComposedWithInverted=function(t){if(this.isNoop()||t.isNoop())return!0;var o=n(this),s=n(t),a=e(this),h=e(t);return!(!a||!h)&&(r(a)&&r(h)?o+a.length===s||o===s:!(!i(a)||!i(h))&&s-h===o)},t.transform=function(e,n){if(e.baseLength!==n.baseLength)throw new Error("Both operations have to have the same base length");for(var s=new t,a=new t,h=e.ops,p=n.ops,c=0,l=0,u=h[c++],f=p[l++];;){if("undefined"==typeof u&&"undefined"==typeof f)break;if(r(u))s.insert(u),a.retain(u.length),u=h[c++];else if(r(f))s.retain(f.length),a.insert(f),f=p[l++];else{if("undefined"==typeof u)throw new Error("Cannot compose operations: first operation is too short.");if("undefined"==typeof f)throw new Error("Cannot compose operations: first operation is too long.");var d;if(o(u)&&o(f))u>f?(d=f,u-=f,f=p[l++]):u===f?(d=f,u=h[c++],f=p[l++]):(d=u,f-=u,u=h[c++]),s.retain(d),a.retain(d);else if(i(u)&&i(f))-u>-f?(u-=f,f=p[l++]):u===f?(u=h[c++],f=p[l++]):(f-=u,u=h[c++]);else if(i(u)&&o(f))-u>f?(d=f,u+=f,f=p[l++]):-u===f?(d=f,u=h[c++],f=p[l++]):(d=-u,f+=u,u=h[c++]),s["delete"](d);else{if(!o(u)||!i(f))throw new Error("The two operations aren't compatible");u>-f?(d=-f,u+=f,f=p[l++]):u===-f?(d=u,u=h[c++],f=p[l++]):(d=u,f+=u,u=h[c++]),a["delete"](d)}}}return[s,a]},t}(),"object"==typeof module&&(module.exports=ot.TextOperation),"undefined"==typeof ot)var ot={};if(ot.Selection=function(t){"use strict";function e(t,e){this.anchor=t,this.head=e}function n(t){this.ranges=t||[]}var o=t.ot?t.ot.TextOperation:require("./text-operation");return e.fromJSON=function(t){return new e(t.anchor,t.head)},e.prototype.equals=function(t){return this.anchor===t.anchor&&this.head===t.head},e.prototype.isEmpty=function(){return this.anchor===this.head},e.prototype.transform=function(t){function n(e){for(var n=e,r=t.ops,i=0,s=t.ops.length;i<s&&(o.isRetain(r[i])?e-=r[i]:o.isInsert(r[i])?n+=r[i].length:(n-=Math.min(e,-r[i]),e+=r[i]),!(e<0));i++);return n}var r=n(this.anchor);return this.anchor===this.head?new e(r,r):new e(r,n(this.head))},n.Range=e,n.createCursor=function(t){return new n([new e(t,t)])},n.fromJSON=function(t){for(var o=t.ranges||t,r=0,i=[];r<o.length;r++)i[r]=e.fromJSON(o[r]);return new n(i)},n.prototype.equals=function(t){if(this.position!==t.position)return!1;if(this.ranges.length!==t.ranges.length)return!1;for(var e=0;e<this.ranges.length;e++)if(!this.ranges[e].equals(t.ranges[e]))return!1;return!0},n.prototype.somethingSelected=function(){for(var t=0;t<this.ranges.length;t++)if(!this.ranges[t].isEmpty())return!0;return!1},n.prototype.compose=function(t){return t},n.prototype.transform=function(t){for(var e=0,o=[];e<this.ranges.length;e++)o[e]=this.ranges[e].transform(t);return new n(o)},n}(this),"object"==typeof module&&(module.exports=ot.Selection),"undefined"==typeof ot)var ot={};if(ot.WrappedOperation=function(t){"use strict";function e(t,e){this.wrapped=t,this.meta=e}function n(t,e){for(var n in t)t.hasOwnProperty(n)&&(e[n]=t[n])}function o(t,e){if(t&&"object"==typeof t){if("function"==typeof t.compose)return t.compose(e);var o={};return n(t,o),n(e,o),o}return e}function r(t,e){return t&&"object"==typeof t&&"function"==typeof t.transform?t.transform(e):t}return e.prototype.apply=function(){return this.wrapped.apply.apply(this.wrapped,arguments)},e.prototype.invert=function(){var t=this.meta;return new e(this.wrapped.invert.apply(this.wrapped,arguments),t&&"object"==typeof t&&"function"==typeof t.invert?t.invert.apply(t,arguments):t)},e.prototype.compose=function(t){return new e(this.wrapped.compose(t.wrapped),o(this.meta,t.meta))},e.transform=function(t,n){var o=t.wrapped.constructor.transform,i=o(t.wrapped,n.wrapped);return[new e(i[0],r(t.meta,n.wrapped)),new e(i[1],r(n.meta,t.wrapped))]},e}(this),"object"==typeof module&&(module.exports=ot.WrappedOperation),"undefined"==typeof ot)var ot={};if(ot.UndoManager=function(){"use strict";function t(t){this.maxItems=t||50,this.state=n,this.dontCompose=!1,this.undoStack=[],this.redoStack=[]}function e(t,e){for(var n=[],o=e.constructor,r=t.length-1;r>=0;r--){var i=o.transform(t[r],e);"function"==typeof i[0].isNoop&&i[0].isNoop()||n.push(i[0]),e=i[1]}return n.reverse()}var n="normal",o="undoing",r="redoing";return t.prototype.add=function(t,e){if(this.state===o)this.redoStack.push(t),this.dontCompose=!0;else if(this.state===r)this.undoStack.push(t),this.dontCompose=!0;else{var n=this.undoStack;!this.dontCompose&&e&&n.length>0?n.push(t.compose(n.pop())):(n.push(t),n.length>this.maxItems&&n.shift()),this.dontCompose=!1,this.redoStack=[]}},t.prototype.transform=function(t){this.undoStack=e(this.undoStack,t),this.redoStack=e(this.redoStack,t)},t.prototype.performUndo=function(t){if(this.state=o,0===this.undoStack.length)throw new Error("undo not possible");t(this.undoStack.pop()),this.state=n},t.prototype.performRedo=function(t){if(this.state=r,0===this.redoStack.length)throw new Error("redo not possible");t(this.redoStack.pop()),this.state=n},t.prototype.canUndo=function(){return 0!==this.undoStack.length},t.prototype.canRedo=function(){return 0!==this.redoStack.length},t.prototype.isUndoing=function(){return this.state===o},t.prototype.isRedoing=function(){return this.state===r},t}(),"object"==typeof module&&(module.exports=ot.UndoManager),"undefined"==typeof ot)var ot={};ot.Client=function(t){"use strict";function e(t){this.revision=t,this.setState(a)}function n(){}function o(t){this.outstanding=t}function r(t,e){this.outstanding=t,this.buffer=e}function i(t,e,n){this.acknowlaged=t,this.client=e,this.revision=n}function s(t,e,n,o){this.acknowlaged=t,this.buffer=e,this.client=n,this.revision=o}e.prototype.setState=function(t){this.state=t},e.prototype.applyClient=function(t){this.setState(this.state.applyClient(this,t))},e.prototype.applyServer=function(t,e){this.setState(this.state.applyServer(this,t,e))},e.prototype.applyOperations=function(t,e){this.setState(this.state.applyOperations(this,t,e))},e.prototype.serverAck=function(t){this.setState(this.state.serverAck(this,t))},e.prototype.serverReconnect=function(){"function"==typeof this.state.resend&&this.state.resend(this)},e.prototype.transformSelection=function(t){return this.state.transformSelection(t)},e.prototype.sendOperation=function(t,e){throw new Error("sendOperation must be defined in child class")},e.prototype.applyOperation=function(t){throw new Error("applyOperation must be defined in child class")},e.Synchronized=n,n.prototype.applyClient=function(t,e){return t.sendOperation(t.revision,e),new o(e)},n.prototype.applyServer=function(t,e,n){if(e-t.revision>1)throw new Error("Invalid revision.");return t.revision=e,t.applyOperation(n),this},n.prototype.serverAck=function(t,e){throw new Error("There is no pending operation.")},n.prototype.transformSelection=function(t){return t};var a=new n;return e.AwaitingConfirm=o,o.prototype.applyClient=function(t,e){return new r(this.outstanding,e)},o.prototype.applyServer=function(t,e,n){if(e-t.revision>1)throw new Error("Invalid revision.");t.revision=e;var r=n.constructor.transform(this.outstanding,n);return t.applyOperation(r[1]),new o(r[0])},o.prototype.serverAck=function(t,e){return e-t.revision>1?new i(this.outstanding,t,e).getOperations():(t.revision=e,a)},o.prototype.transformSelection=function(t){return t.transform(this.outstanding)},o.prototype.resend=function(t){t.sendOperation(t.revision,this.outstanding)},e.AwaitingWithBuffer=r,r.prototype.applyClient=function(t,e){var n=this.buffer.compose(e);return new r(this.outstanding,n)},r.prototype.applyServer=function(t,e,n){if(e-t.revision>1)throw new Error("Invalid revision.");t.revision=e;var o=n.constructor.transform,i=o(this.outstanding,n),s=o(this.buffer,i[1]);return t.applyOperation(s[1]),new r(i[0],s[0])},r.prototype.serverAck=function(t,e){return e-t.revision>1?new s(this.outstanding,this.buffer,t,e).getOperations():(t.revision=e,t.sendOperation(t.revision,this.buffer),new o(this.buffer))},r.prototype.transformSelection=function(t){return t.transform(this.outstanding).transform(this.buffer)},r.prototype.resend=function(t){t.sendOperation(t.revision,this.outstanding)},e.Stale=i,i.prototype.applyClient=function(t,e){return new s(this.acknowlaged,e,t,this.revision)},i.prototype.applyServer=function(t,e,n){throw new Error("Ignored server-side change.")},i.prototype.applyOperations=function(t,e,n){for(var o=this.acknowlaged.constructor.transform,r=0;r<n.length;r++){var i=ot.TextOperation.fromJSON(n[r]),s=o(this.acknowlaged,i);t.applyOperation(s[1]),this.acknowlaged=s[0]}return t.revision=this.revision,a},i.prototype.serverAck=function(t,e){throw new Error("There is no pending operation.")},i.prototype.transformSelection=function(t){return t},i.prototype.getOperations=function(){return this.client.getOperations(this.client.revision,this.revision-1),this},e.StaleWithBuffer=s,s.prototype.applyClient=function(t,e){var n=this.buffer.compose(e);return new s(this.acknowlaged,n,t,this.revision)},s.prototype.applyServer=function(t,e,n){throw new Error("Ignored server-side change.")},s.prototype.applyOperations=function(t,e,n){for(var r=this.acknowlaged.constructor.transform,i=0;i<n.length;i++){var s=ot.TextOperation.fromJSON(n[i]),a=r(this.acknowlaged,s),h=r(this.buffer,a[1]);t.applyOperation(h[1]),this.acknowlaged=a[0],this.buffer=h[0]}return t.revision=this.revision,t.sendOperation(t.revision,this.buffer),new o(this.buffer)},s.prototype.serverAck=function(t,e){throw new Error("There is no pending operation.")},s.prototype.transformSelection=function(t){return t},s.prototype.getOperations=function(){return this.client.getOperations(this.client.revision,this.revision-1),this},e}(this),"object"==typeof module&&(module.exports=ot.Client),ot.CodeMirrorAdapter=function(t){"use strict";function e(t){this.cm=t,this.ignoreNextChange=!1,this.changeInProgress=!1,this.selectionChanged=!1,a(this,"onChanges"),a(this,"onChange"),a(this,"onCursorActivity"),a(this,"onFocus"),a(this,"onBlur"),t.on("changes",this.onChanges),t.on("change",this.onChange),t.on("cursorActivity",this.onCursorActivity),t.on("focus",this.onFocus),t.on("blur",this.onBlur)}function n(t,e){return t.line<e.line?-1:t.line>e.line?1:t.ch<e.ch?-1:t.ch>e.ch?1:0}function o(t,e){return n(t,e)<=0}function r(t,e){return o(t,e)?t:e}function i(t,e){return o(t,e)?e:t}function s(t){return t.indexFromPos({line:t.lastLine(),ch:0})+t.getLine(t.lastLine()).length}function a(t,e){var n=t[e];t[e]=function(){n.apply(t,arguments)}}var h=ot.TextOperation,p=ot.Selection;e.prototype.detach=function(){this.cm.off("changes",this.onChanges),this.cm.off("change",this.onChange),this.cm.off("cursorActivity",this.onCursorActivity),this.cm.off("focus",this.onFocus),this.cm.off("blur",this.onBlur)},e.operationFromCodeMirrorChanges=function(t,e){function n(t){return t[t.length-1]}function r(t){if(0===t.length)return 0;for(var e=0,n=0;n<t.length;n++)e+=t[n].length;return e+t.length-1}function i(t,e){return function(i){return o(i,e.from)?t(i):o(e.to,i)?t({line:i.line+e.text.length-1-(e.to.line-e.from.line),ch:e.to.line<i.line?i.ch:e.text.length<=1?i.ch-(e.to.ch-e.from.ch)+r(e.text):i.ch-e.to.ch+n(e.text).length})+r(e.removed)-r(e.text):e.from.line===i.line?t(e.from)+i.ch-e.from.ch:t(e.from)+r(e.removed.slice(0,i.line-e.from.line))+1+i.ch}}for(var a=s(e),p=(new h).retain(a),c=(new h).retain(a),l=function(t){return e.indexFromPos(t)},u=t.length-1;u>=0;u--){var f=t[u];l=i(l,f);var d=l(f.from),g=a-d-r(f.text);p=(new h).retain(d)["delete"](r(f.removed)).insert(f.text.join("\n")).retain(g).compose(p),c=c.compose((new h).retain(d)["delete"](r(f.text)).insert(f.removed.join("\n")).retain(g)),a+=r(f.removed)-r(f.text)}return[p,c]},e.operationFromCodeMirrorChange=e.operationFromCodeMirrorChanges,e.applyOperationToCodeMirror=function(t,e){e.operation(function(){for(var n=t.ops,o=0,r=0,i=n.length;r<i;r++){var s=n[r];if(h.isRetain(s))o+=s;else if(h.isInsert(s))e.replaceRange(s,e.posFromIndex(o),null,"ignoreHistory"),o+=s.length;else if(h.isDelete(s)){var a=e.posFromIndex(o),p=e.posFromIndex(o-s);e.replaceRange("",a,p,"ignoreHistory")}}})},e.prototype.registerCallbacks=function(t){this.callbacks=t},e.prototype.onChange=function(){this.changeInProgress=!0},e.prototype.onChanges=function(t,n){if(!this.ignoreNextChange){var o=e.operationFromCodeMirrorChanges(n,this.cm);this.trigger("change",o[0],o[1])}this.selectionChanged&&this.trigger("selectionChange"),this.changeInProgress=!1,this.ignoreNextChange=!1},e.prototype.onCursorActivity=e.prototype.onFocus=function(){this.changeInProgress?this.selectionChanged=!0:this.trigger("selectionChange")},e.prototype.onBlur=function(){this.cm.somethingSelected()||this.trigger("blur")},e.prototype.getValue=function(){return this.cm.getValue()},e.prototype.getSelection=function(){for(var t=this.cm,e=t.listSelections(),n=[],o=0;o<e.length;o++)n[o]=new p.Range(t.indexFromPos(e[o].anchor),t.indexFromPos(e[o].head));return new p(n)},e.prototype.setSelection=function(t){for(var e=[],n=0;t&&n<t.ranges.length;n++){var o=t.ranges[n];e[n]={anchor:this.cm.posFromIndex(o.anchor),head:this.cm.posFromIndex(o.head)}}this.cm.setSelections(e)};var c=function(){var t={},e=document.createElement("style");document.documentElement.getElementsByTagName("head")[0].appendChild(e);var n=e.sheet;return function(e){t[e]||(t[e]=!0,n.insertRule(e,(n.cssRules||n.rules).length))}}();return e.prototype.setOtherCursor=function(t,e,n){var o=this.cm.posFromIndex(t),r=(this.cm.cursorCoords(o),document.createElement("span"));return r.className="other-client",r.style.display="none",r.setAttribute("data-clientid",n),this.cm.setBookmark(o,{widget:r,insertLeft:!0})},e.prototype.setOtherSelectionRange=function(t,e,n){var o=/^#([0-9a-fA-F]{6})$/.exec(e);if(!o)throw new Error("only six-digit hex colors are allowed.");var s="selection-"+o[1],a=hex2rgb(e),h="."+s+" { background: rgba("+a.red+","+a.green+","+a.blue+",0.2); }";c(h);var p=this.cm.posFromIndex(t.anchor),l=this.cm.posFromIndex(t.head);return this.cm.markText(r(p,l),i(p,l),{className:s})},e.prototype.setOtherSelection=function(t,e,n){for(var o=[],r=0;r<t.ranges.length;r++){var i=t.ranges[r];i.isEmpty()||(o[r]=this.setOtherSelectionRange(i,e,n))}return{clear:function(){for(var t=0;t<o.length;t++)o[t]&&o[t].clear()}}},e.prototype.trigger=function(t){var e=Array.prototype.slice.call(arguments,1),n=this.callbacks&&this.callbacks[t];n&&n.apply(this,e)},e.prototype.applyOperation=function(t){t.isNoop()||(this.ignoreNextChange=!0),e.applyOperationToCodeMirror(t,this.cm)},e.prototype.registerUndo=function(t){this.cm.undo=t},e.prototype.registerRedo=function(t){this.cm.redo=t},e}(this),ot.SocketIOAdapter=function(){"use strict";function t(t){this.socket=t;var e=this;t.on("client_left",function(t){e.trigger("client_left",t)}),t.on("set_name",function(t,n){e.trigger("set_name",t,n)}),t.on("set_color",function(t,n){e.trigger("set_color",t,n)}),t.on("ack",function(t){e.trigger("ack",t)}),t.on("operation",function(t,n,o,r){e.trigger("operation",n,o),e.trigger("selection",t,r)}),t.on("operations",function(t,n){e.trigger("operations",t,n)}),t.on("selection",function(t,n){e.trigger("selection",t,n)}),t.on("reconnect",function(){e.trigger("reconnect")})}return t.prototype.sendOperation=function(t,e,n){this.socket.emit("operation",t,e,n)},t.prototype.sendSelection=function(t){this.socket.emit("selection",t)},t.prototype.getOperations=function(t,e){this.socket.emit("get_operations",t,e)},t.prototype.registerCallbacks=function(t){this.callbacks=t},t.prototype.trigger=function(t){var e=Array.prototype.slice.call(arguments,1),n=this.callbacks&&this.callbacks[t];n&&n.apply(this,e)},t}(),ot.AjaxAdapter=function(){"use strict";function t(t,e,n){"/"!==t[t.length-1]&&(t+="/"),this.path=t,this.ownUserName=e,this.majorRevision=n.major||0,this.minorRevision=n.minor||0,this.poll()}return t.prototype.renderRevisionPath=function(){return"revision/"+this.majorRevision+"-"+this.minorRevision},t.prototype.handleResponse=function(t){var e,n=t.operations;for(e=0;e<n.length;e++)n[e].user===this.ownUserName?this.trigger("ack"):this.trigger("operation",n[e].operation);n.length>0&&(this.majorRevision+=n.length,this.minorRevision=0);var o=t.events;if(o){for(e=0;e<o.length;e++){var r=o[e].user;if(r!==this.ownUserName)switch(o[e].event){case"joined":this.trigger("set_name",r,r);break;case"left":this.trigger("client_left",r);break;case"selection":this.trigger("selection",r,o[e].selection)}}this.minorRevision+=o.length}var i=t.users;i&&(delete i[this.ownUserName],this.trigger("clients",i)),t.revision&&(this.majorRevision=t.revision.major,this.minorRevision=t.revision.minor)},t.prototype.poll=function(){var t=this;$.ajax({url:this.path+this.renderRevisionPath(),type:"GET",dataType:"json",timeout:5e3,success:function(e){t.handleResponse(e),t.poll()},error:function(){setTimeout(function(){t.poll()},500)}})},t.prototype.sendOperation=function(t,e,n){if(t!==this.majorRevision)throw new Error("Revision numbers out of sync");var o=this;$.ajax({url:this.path+this.renderRevisionPath(),type:"POST",data:JSON.stringify({operation:e,selection:n}),contentType:"application/json",processData:!1,success:function(t){},error:function(){setTimeout(function(){o.sendOperation(t,e,n)},500)}})},t.prototype.sendSelection=function(t){$.ajax({url:this.path+this.renderRevisionPath()+"/selection",type:"POST",data:JSON.stringify(t),contentType:"application/json",processData:!1,timeout:1e3})},t.prototype.registerCallbacks=function(t){this.callbacks=t},t.prototype.trigger=function(t){var e=Array.prototype.slice.call(arguments,1),n=this.callbacks&&this.callbacks[t];n&&n.apply(this,e)},t}(),ot.EditorClient=function(){"use strict";function t(t,e){this.selectionBefore=t,this.selectionAfter=e}function e(t,e){this.clientId=t,this.selection=e}function n(t,e,n,o,r,i){this.id=t,this.listEl=e,this.editorAdapter=n,this.name=o,this.color=r,this.li=document.createElement("li"),o&&(this.li.textContent=o,this.listEl.appendChild(this.li)),r?this.setForceColor(r):this.setColor(o?s(o):Math.random()),i&&this.updateSelection(i)}function o(t,e,n,o){c.call(this,t),this.serverAdapter=n,this.editorAdapter=o,this.undoManager=new u,this.initializeClientList(),this.initializeClients(e);var r=this;this.editorAdapter.registerCallbacks({change:function(t,e){r.onChange(t,e)},selectionChange:function(){r.onSelectionChange()},blur:function(){r.onBlur()}}),this.editorAdapter.registerUndo(function(){r.undo()}),this.editorAdapter.registerRedo(function(){r.redo()}),this.serverAdapter.registerCallbacks({client_left:function(t){r.onClientLeft(t)},set_name:function(t,e){r.getClientObject(t).setName(e)},set_color:function(t,e){r.getClientObject(t).setForceColor(e)},ack:function(t){r.serverAck(t)},operation:function(t,e){r.applyServer(t,f.fromJSON(e))},operations:function(t,e){r.applyOperations(t,e)},selection:function(t,e){e?r.getClientObject(t).updateSelection(r.transformSelection(l.fromJSON(e))):r.getClientObject(t).removeSelection()},clients:function(t){var e;for(e in r.clients)r.clients.hasOwnProperty(e)&&!t.hasOwnProperty(e)&&r.onClientLeft(e);for(e in t)if(t.hasOwnProperty(e)){var n=r.getClientObject(e);t[e].name&&n.setName(t[e].name);var o=t[e].selection;o?r.clients[e].updateSelection(r.transformSelection(l.fromJSON(o))):r.clients[e].removeSelection()}},reconnect:function(){r.serverReconnect()}})}function r(t,e,n){function o(t){var e=Math.round(255*t).toString(16);return 1===e.length?"0"+e:e}return"#"+o(t)+o(e)+o(n)}function i(t,e,n){if(0===e)return r(n,n,n);var o=n<.5?n*(1+e):n+e-e*n,i=2*n-o,s=function(t){return t<0&&(t+=1),t>1&&(t-=1),6*t<1?i+6*(o-i)*t:2*t<1?o:3*t<2?i+6*(o-i)*(2/3-t):i};return r(s(t+1/3),s(t),s(t-1/3))}function s(t){for(var e=1,n=0;n<t.length;n++)e=17*(e+t.charCodeAt(n))%360;return e/360}function a(t,e){function n(){}n.prototype=e.prototype,t.prototype=new n,t.prototype.constructor=t}function h(t){return t[t.length-1]}function p(t){t.parentNode&&t.parentNode.removeChild(t)}var c=ot.Client,l=ot.Selection,u=ot.UndoManager,f=ot.TextOperation,d=ot.WrappedOperation;return t.prototype.invert=function(){return new t(this.selectionAfter,this.selectionBefore)},t.prototype.compose=function(e){return new t(this.selectionBefore,e.selectionAfter)},t.prototype.transform=function(e){return new t(this.selectionBefore?this.selectionBefore.transform(e):null,this.selectionAfter?this.selectionAfter.transform(e):null)},e.fromJSON=function(t){return new e(t.clientId,t.selection&&l.fromJSON(t.selection))},e.prototype.transform=function(t){return new e(this.clientId,this.selection&&this.selection.transform(t))},n.prototype.setColor=function(t){this.hue=t,this.color=i(t,.75,.5),this.lightColor=i(t,.5,.9),this.li&&(this.li.style.color=this.color)},n.prototype.setForceColor=function(t){this.hue=null,this.color=t,this.lightColor=t,this.li&&(this.li.style.color=this.color)},n.prototype.setName=function(t){this.name!==t&&(this.name=t,this.li.textContent=t,this.li.parentNode||this.listEl.appendChild(this.li),this.setColor(s(t)))},n.prototype.updateSelection=function(t){this.removeSelection(),this.selection=t,this.mark=this.editorAdapter.setOtherSelection(t,t.position===t.selectionEnd?this.color:this.lightColor,this.id)},n.prototype.remove=function(){this.li&&p(this.li),this.removeSelection()},n.prototype.removeSelection=function(){this.mark&&(this.mark.clear(),this.mark=null)},a(o,c),o.prototype.addClient=function(t,e){this.clients[t]=new n(t,this.clientListEl,this.editorAdapter,e.name||t,e.color||null,e.selection?l.fromJSON(e.selection):null)},o.prototype.initializeClients=function(t){this.clients={};for(var e in t)t.hasOwnProperty(e)&&this.addClient(e,t[e])},o.prototype.getClientObject=function(t){var e=this.clients[t];return e?e:this.clients[t]=new n(t,this.clientListEl,this.editorAdapter)},o.prototype.onClientLeft=function(t){var e=this.clients[t];e&&(e.remove(),delete this.clients[t])},o.prototype.initializeClientList=function(){this.clientListEl=document.createElement("ul")},o.prototype.applyUnredo=function(t){this.undoManager.add(t.invert(this.editorAdapter.getValue())),this.editorAdapter.applyOperation(t.wrapped),this.selection=t.meta.selectionAfter,this.editorAdapter.setSelection(this.selection),this.applyClient(t.wrapped)},o.prototype.undo=function(){var t=this;this.undoManager.canUndo()&&this.undoManager.performUndo(function(e){t.applyUnredo(e)})},o.prototype.redo=function(){var t=this;this.undoManager.canRedo()&&this.undoManager.performRedo(function(e){t.applyUnredo(e)})},o.prototype.onChange=function(e,n){var o=this.selection;this.updateSelection();var r=new t(o,this.selection),i=(new d(e,r),this.undoManager.undoStack.length>0&&n.shouldBeComposedWithInverted(h(this.undoManager.undoStack).wrapped)),s=new t(this.selection,o);this.undoManager.add(new d(n,s),i),this.applyClient(e)},o.prototype.updateSelection=function(){this.selection=this.editorAdapter.getSelection()},o.prototype.onSelectionChange=function(){var t=this.selection;this.updateSelection(),t&&this.selection.equals(t)||this.sendSelection(this.selection)},o.prototype.onBlur=function(){this.selection=null,this.sendSelection(null)},o.prototype.sendSelection=function(t){this.state instanceof c.AwaitingWithBuffer||this.serverAdapter.sendSelection(t)},o.prototype.sendOperation=function(t,e){this.serverAdapter.sendOperation(t,e.toJSON(),this.selection)},o.prototype.getOperations=function(t,e){this.serverAdapter.getOperations(t,e)},o.prototype.applyOperation=function(t){this.editorAdapter.applyOperation(t),this.updateSelection(),this.undoManager.transform(new d(t,null))},o}();
\ No newline at end of file
diff --git a/public/views/error.ejs b/public/views/error.ejs
index 402b5eb..a40ed39 100644
--- a/public/views/error.ejs
+++ b/public/views/error.ejs
@@ -2,18 +2,18 @@
 <html lang="en">
 
 <head>
-    <%- include head %>
+    <%- include hackmd/head %>
     <link rel="stylesheet" href="<%- url %>/css/center.css">
 </head>
 
 <body>
-    <%- include header %>
+    <%- include hackmd/header %>
     <div class="container-fluid text-center">
         <div class="vertical-center-row">
                 <h1><%- code %> <%- detail %> <small><%- msg %></small></h1>
         </div>
     </div>
-    <%- include footer %>
+    <%- include hackmd/footer %>
 </body>
 
 </html>
\ No newline at end of file
diff --git a/public/views/hackmd.ejs b/public/views/hackmd.ejs
index c5778fc..49084a6 100644
--- a/public/views/hackmd.ejs
+++ b/public/views/hackmd.ejs
@@ -2,14 +2,14 @@
 <html lang="en">
 
 <head>
-    <%- include head %>
+    <%- include hackmd/head %>
 </head>
 
 <body>
-    <%- include header %>
-    <%- include body %>
-    <%- include footer %>
-    <%- include foot %>
+    <%- include hackmd/header %>
+    <%- include hackmd/body %>
+    <%- include hackmd/footer %>
+    <%- include hackmd/foot %>
 </body>
 
-</html>
\ No newline at end of file
+</html>
diff --git a/public/views/body.ejs b/public/views/hackmd/body.ejs
similarity index 99%
rename from public/views/body.ejs
rename to public/views/hackmd/body.ejs
index 5ad1733..d8a3f10 100644
--- a/public/views/body.ejs
+++ b/public/views/hackmd/body.ejs
@@ -244,7 +244,7 @@
         </div>
     </div>
 </div>
-<%- include refresh-modal %>
-<%- include signin-modal %>
-<%- include help-modal %>
-<%- include revision-modal %>
+<%- include ../shared/refresh-modal %>
+<%- include ../shared/signin-modal %>
+<%- include ../shared/help-modal %>
+<%- include ../shared/revision-modal %>
diff --git a/public/views/foot.ejs b/public/views/hackmd/foot.ejs
similarity index 88%
rename from public/views/foot.ejs
rename to public/views/hackmd/foot.ejs
index c1df65c..445b021 100644
--- a/public/views/foot.ejs
+++ b/public/views/hackmd/foot.ejs
@@ -11,18 +11,18 @@
 <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js" integrity="sha256-yYfngbEKv4RENfGDvNUqJTqGFcKf31NJEe9OTnnMH3Y=" crossorigin="anonymous" defer></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/config/TeX-AMS-MML_HTMLorMML.js" integrity="sha256-immzXfCGLhnx3Zfi9F/dUcqxEM8K3o3oTFy9Bh6HCwg=" crossorigin="anonymous" defer></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment-with-locales.min.js" integrity="sha256-vvT7Ok9u6GbfnBPXnbM6FVDEO8E1kTdgHOFZOAXrktA=" crossorigin="anonymous" defer></script>
-<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/6.0.0/mermaid.min.js" integrity="sha256-Yabf6Mj1TPKd6h4F6z5xRR1/2son0Wg8NhvjYnhcQcY=" crossorigin="anonymous" defer></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/7.0.0/mermaid.min.js" integrity="sha256-1uR+pqxH5fN/rOZcZTb9c5+bR3OIYEKzu2sI11Dnj9A=" crossorigin="anonymous" defer></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/emojify.js/1.1.0/js/emojify.min.js" integrity="sha256-VAB5tAlKBvgaxw8oJ1crWMVbdmBVl4mP/2M8MNRl+4E=" crossorigin="anonymous" defer></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.2/lodash.min.js" integrity="sha256-Cv5v4i4SuYvwRYzIONifZjoc99CkwfncROMSWat1cVA=" crossorigin="anonymous" defer></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/1.7.2/socket.io.min.js" integrity="sha256-WKvqiY0jZHWQZIohYEmr9KUC5rEaYEOFTq+ByllJK8w=" crossorigin="anonymous" defer></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.6/handlebars.min.js" integrity="sha256-1O3BtOwnPyyRzOszK6P+gqaRoXHV6JXj8HkjZmPYhCI=" crossorigin="anonymous" defer></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.9.0/highlight.min.js" integrity="sha256-KbfTjB0WZ8vvXngdpJGY3Yp3xKk+tttbqClO11anCIU=" crossorigin="anonymous" defer></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/gist-embed/2.6.0/gist-embed.min.js" integrity="sha256-KyF2D6xPIJUW5sUDSs93vWyZm+1RzIpKCexxElmxl8g=" crossorigin="anonymous" defer></script>
-<script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/1.4.1/viz.js" integrity="sha256-U0a9HpXT7zG0N3tVzo58B5S+QXUxo4FdBIjrBMYrxZI=" crossorigin="anonymous" defer></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/1.7.0/viz.js" integrity="sha256-8t+rndrF+TU4JtelmOH1lDHTMe2ovhO2UbzDArp5lY8=" crossorigin="anonymous" defer></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/1000hz-bootstrap-validator/0.11.8/validator.min.js" integrity="sha256-LHeY7YoYJ0SSXbCx7sR14Pqna+52moaH3bhv0Mjzd/M=" crossorigin="anonymous" defer></script>
-<%- include build/index-scripts %>
+<%- include ../build/index-scripts %>
 <% } else { %>
 <script src="<%- url %>/build/MathJax/MathJax.js" defer></script>
 <script src="<%- url %>/build/MathJax/config/TeX-AMS-MML_HTMLorMML.js" defer></script>
-<%- include build/index-pack-scripts %>
+<%- include ../build/index-pack-scripts %>
 <% } %>
diff --git a/public/views/footer.ejs b/public/views/hackmd/footer.ejs
similarity index 100%
rename from public/views/footer.ejs
rename to public/views/hackmd/footer.ejs
diff --git a/public/views/head.ejs b/public/views/hackmd/head.ejs
similarity index 93%
rename from public/views/head.ejs
rename to public/views/hackmd/head.ejs
index 218847f..d066399 100644
--- a/public/views/head.ejs
+++ b/public/views/hackmd/head.ejs
@@ -14,9 +14,9 @@
 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/ionicons/2.0.1/css/ionicons.min.css" integrity="sha256-3iu9jgsy9TpTwXKb7bNQzqWekRX7pPK+2OLj3R922fo=" crossorigin="anonymous" />
 <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/octicons/3.5.0/octicons.min.css" integrity="sha256-QiWfLIsCT02Sdwkogf6YMiQlj4NE84MKkzEMkZnMGdg=" crossorigin="anonymous" />
 <link rel="stylesheet" href='<%- url %>/build/emojify.js/dist/css/basic/emojify.min.css'>
-<%- include build/index-header %>
+<%- include ../build/index-header %>
 <% } else { %>
 <link rel="stylesheet" href='<%- url %>/build/emojify.js/dist/css/basic/emojify.min.css'>
-<%- include build/index-pack-header %>
+<%- include ../build/index-pack-header %>
 <% } %>
-<%- include polyfill %>
\ No newline at end of file
+<%- include ../shared/polyfill %>
\ No newline at end of file
diff --git a/public/views/header.ejs b/public/views/hackmd/header.ejs
similarity index 100%
rename from public/views/header.ejs
rename to public/views/hackmd/header.ejs
diff --git a/public/views/index.ejs b/public/views/index.ejs
index 2513990..5732db4 100644
--- a/public/views/index.ejs
+++ b/public/views/index.ejs
@@ -2,219 +2,14 @@
 <html lang="en">
 
 <head>
-    <meta charset="utf-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge">
-    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
-    <meta name="apple-mobile-web-app-capable" content="yes">
-    <meta name="apple-mobile-web-app-status-bar-style" content="black">
-    <meta name="mobile-web-app-capable" content="yes">
-    <meta name="description" content="<%= __('Best way to write and share your knowledge in markdown.') %>">
-    <meta name="keywords" content="Collaborative, Markdown, Notes">
-    <title>HackMD - <%= __('Collaborative markdown notes') %></title>
-    <link rel="icon" type="image/png" href="<%- url %>/favicon.png">
-    <link rel="apple-touch-icon" href="<%- url %>/apple-touch-icon.png">
-    <% if(useCDN) { %>
-    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha256-916EbMg70RQy9LHiGkXzG8hSg9EdNy97GazNG/aiY1w=" crossorigin="anonymous" />
-    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" integrity="sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0=" crossorigin="anonymous" />
-	<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-social/4.9.0/bootstrap-social.min.css" integrity="sha256-02JtFTurpwBjQJ6q13iJe82/NF0RbZlJroDegK5g87Y=" crossorigin="anonymous" />
-    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/select2/3.5.4/select2.min.css" integrity="sha256-ijlUKKj3hJCiiT2HWo1kqkI79NTEYpzOsw5Rs3k42dI=" crossorigin="anonymous" />
-    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/select2/3.5.4/select2-bootstrap.min.css" integrity="sha256-NAWFcNIZdH+TS1xpWujF/EB/Y8gwBbEOCoaK/eqaer8=" crossorigin="anonymous" />
-    <%- include build/cover-header %>
-	<% } else { %>
-	<%- include build/cover-pack-header %>
-    <% } %>
-    <%- include polyfill %>
+    <%- include index/head %>
 </head>
 
 <body>
-    <div class="site-wrapper">
-        <div class="site-wrapper-inner">
-            <div class="cover-container">
-
-                <div class="masthead clearfix">
-                    <div class="inner">
-                        <h3 class="masthead-brand"></h3>
-                        <nav>
-                            <ul class="nav masthead-nav">
-                                <li class="ui-home<% if(!signin) { %> active<% } %>"><a href="#"><%= __('Intro') %></a>
-                                </li>
-                                <li class="ui-history<% if(signin) { %> active<% } %>"><a href="#"><%= __('History') %></a>
-                                </li>
-                                <div class="ui-signin" style="float: right; margin-top: 8px;<% if(signin) { %> display: none;<% } %>">
-                                    <% if(allowAnonymous) { %>
-                                    <a type="button" href="<%- url %>/new" class="btn btn-sm btn-link"><i class="fa fa-plus"></i> <%= __('New guest note') %></a>
-                                    <% } %>
-                                    <% if(facebook || twitter || github || gitlab || dropbox || google || ldap || email) { %>
-                                    <button class="btn btn-sm btn-success ui-signin" data-toggle="modal" data-target=".signin-modal"><%= __('Sign In') %></button>
-                                    <% } %>
-                                </div>
-                                <div class="ui-signout" style="float: right; margin-top: 8px;<% if(!signin) { %> display: none;<% } %>">
-                                    <a type="button" href="<%- url %>/new" class="btn btn-sm btn-link"><i class="fa fa-plus"></i> <%= __('New note') %></a>
-                                    <span class="ui-profile dropdown pull-right">
-                                        <button id="profileLabel" class="btn btn-sm btn-link ui-profile-label" style="padding-right: 0;" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
-                                            <img class="ui-avatar" width="20" height="20"><span class="hidden-xs hidden-sm">&ensp;<span class="ui-name"></span></span>&ensp;<i class="fa fa-caret-down"></i>
-                                        </button>
-                                        <ul class="dropdown-menu" aria-labelledby="profileLabel">
-                                            <li><a href="<%- url %>/logout"><i class="fa fa-sign-out fa-fw"></i> <%= __('Sign Out') %></a></li>
-                                        </ul>
-                                    </span>
-                                </div>
-                            </ul>
-                        </nav>
-                    </div>
-                </div>
-
-                <div id="home" class="section"<% if(signin) { %> style="display:none;"<% } %>>
-                    <div class="inner cover">
-                        <h1 class="cover-heading"><i class="fa fa-file-text"></i> HackMD</h1>
-                        <p class="lead">
-                            <%= __('Best way to write and share your knowledge in markdown.') %>
-                        </p>
-                        <% if (infoMessage && infoMessage.length > 0) { %>
-                        <div class="alert alert-info" style="max-width: 400px; margin: 0 auto;"><%= infoMessage %></div>
-                        <% } %>
-                        <% if (errorMessage && errorMessage.length > 0) { %>
-                        <div class="alert alert-danger" style="max-width: 400px; margin: 0 auto;"><%= errorMessage %></div>
-                        <% } %>
-                        <% if(facebook || twitter || github || gitlab || dropbox || google || ldap || email) { %>
-                        <span class="ui-signin">
-                            <br>
-                            <a type="button" class="btn btn-lg btn-success ui-signin" data-toggle="modal" data-target=".signin-modal" style="min-width: 200px;"><%= __('Sign In') %></a>
-                        </span>
-                        <span class="ui-or"><%= __('or') %></span>
-                        <% } %>
-                        <span class="ui-signin">
-                            <a type="button" href="<%- url %>/features" class="btn btn-lg btn-primary" style="min-width: 200px;"><%= __('Explore all features') %></a>
-                            <br>
-                            <br>
-                        </span>
-                        <div class="lead row" style="width: 90%; margin: 0 auto;">
-                            <div class="col-md-4 inner">
-                                <a href="<%- url %>/features#share-notes">
-                                    <i class="fa fa-bolt fa-3x"></i>
-                                    <h4><%= __('Collaborate with URL') %></h4>
-                                </a>
-                            </div>
-                            <div class="col-md-4 inner">
-                                <a href="<%- url %>/features#mathjax">
-                                    <i class="fa fa-bar-chart fa-3x"></i>
-                                    <h4><%= __('Support charts and MathJax') %></h4>
-                                </a>
-                            </div>
-                            <div class="col-md-4 inner">
-                                <a href="<%- url %>/features#slide-mode">
-                                    <i class="fa fa-tv fa-3x"></i>
-                                    <h4><%= __('Support slide mode') %></h4>
-                                </a>
-                            </div>
-                        </div>
-                    </div>
-                </div>
-
-                <div id="history" class="section"<% if(!signin) { %> style="display:none;"<% } %>>
-                    <div class="ui-signin"<% if(signin) { %> style="display:none;"<% } %>>
-                        <p><%= __('Below is the history from browser') %></p>
-                    </div>
-                    <br>
-                    <form class="form-inline">
-                        <div class="form-group" style="vertical-align: bottom;">
-                            <input class="form-control ui-use-tags" placeholder="<%= __('Select tags...') %>" />
-                        </div>
-                        <div class="form-group">
-                            <input class="search form-control" placeholder="<%= __('Search keyword...') %>" />
-                        </div>
-                        <a href="#" class="sort btn btn-default" data-sort="text" title="<%= __('Sort by title') %>">
-                        <%= __('Title') %>
-                        </a>
-                        <a href="#" class="sort btn btn-default" data-sort="timestamp" title="<%= __('Sort by time') %>">
-                        <%= __('Time') %>
-                        </a>
-                        <span class="hidden-xs hidden-sm">
-                            <a href="#" class="btn btn-default ui-save-history" title="<%= __('Export history') %>"><i class="fa fa-save"></i></a>
-                            <span class="btn btn-default btn-file ui-open-history" title="<%= __('Import history') %>">
-                                <i class="fa fa-folder-open-o"></i><input type="file" />
-                            </span>
-                        <a href="#" class="btn btn-default ui-clear-history" title="<%= __('Clear history') %>" data-toggle="modal" data-target=".delete-modal"><i class="fa fa-trash-o"></i></a>
-                        </span>
-                        <a href="#" class="btn btn-default ui-refresh-history" title="<%= __('Refresh history') %>"><i class="fa fa-refresh"></i></a>
-                    </form>
-                    <h4 class="ui-nohistory" style="display:none;">
-                        <%= __('No history') %>
-                    </h4>
-                    <a href="#" class="btn btn-primary ui-import-from-browser" style="display:none;"><%= __('Import from browser') %></a>
-                    <ul id="history-list" class="list">
-                    </ul>
-                    <ul class="pagination"></ul>
-                </div>
-
-                <div class="mastfoot">
-                    <div class="inner">
-                        <h6 class="social-foot">
-                            <iframe src="//ghbtns.com/github-btn.html?user=hackmdio&repo=hackmd&type=star&count=true" frameborder="0" scrolling="0" width="104px" height="20px"></iframe>
-                        </h6>
-                        <p>
-                            &copy; 2017 <a href="https://www.facebook.com/hackmdio" target="_blank"><i class="fa fa-facebook-square"></i> HackMD</a> | <a href="<%- url %>/s/release-notes" target="_blank"><%= __('Releases') %></a>
-                        </p>
-                        <select class="ui-locale">
-                            <option value="en">English</option>
-                            <option value="zh">中文</option>
-                            <option value="fr">Français</option>
-                            <option value="de">Deutsch</option>
-                            <option value="ja">日本語</option>
-                            <option value="es">Español</option>
-                            <option value="el">Ελληνικά</option>
-                            <option value="pt">Português</option>
-                            <option value="it">italiano</option>
-                            <option value="tr">Türkçe</option>
-                            <option value="ru">Русский</option>
-                            <option value="nl">Nederlands</option>
-                            <option value="hr">hrvatski jezik</option>
-                            <option value="pl">język polski</option>
-                            <option value="uk">Українська</option>
-                            <option value="hi">हिन्दी</option>
-                            <option value="sv">svenska</option>
-							<option value="eo">Esperanto</option>
-                        </select>
-                    </div>
-                </div>
-            </div>
-        </div>
-    </div>
-    <!-- delete modal -->
-    <div class="modal fade delete-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
-        <div class="modal-dialog modal-sm">
-            <div class="modal-content">
-                <div class="modal-header">
-                    <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
-                    </button>
-                    <h4 class="modal-title" id="myModalLabel"><%= __('Are you sure?') %></h4>
-                </div>
-                <div class="modal-body" style="color:black;">
-                    <h5 class="ui-delete-modal-msg"></h5>
-                    <strong class="ui-delete-modal-item"></strong>
-                </div>
-                <div class="modal-footer">
-                    <button type="button" class="btn btn-default" data-dismiss="modal"><%= __('Cancel') %></button>
-                    <button type="button" class="btn btn-danger ui-delete-modal-confirm"><%= __('Yes, do it!') %></button>
-                </div>
-            </div>
-        </div>
-    </div>
-    <%- include signin-modal %>
-
-    <% if(useCDN) { %>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.4.0/velocity.min.js" integrity="sha256-bhm0lgEt6ITaZCDzZpkr/VXVrLa5RP4u9v2AYsbzSUk=" crossorigin="anonymous" defer></script>
-	<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha256-U5ZEeKfGNOja007MMD3YBI0A3OSZOQbeG6z2f2Y0hu8=" crossorigin="anonymous" defer></script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/list.pagination.js/0.1.1/list.pagination.min.js" integrity="sha256-WwTza96H3BgcQTfEfxX7MFaFc/dZA0QrPRKDRLdFHJo=" crossorigin="anonymous" defer></script>
-	<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/3.5.2/select2.min.js" integrity="sha256-HzzZFiY4t0PIv02Tm8/R3CVvLpcjHhO1z/YAUCp4oQ4=" crossorigin="anonymous" defer></script>
-	<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment-with-locales.min.js" integrity="sha256-vvT7Ok9u6GbfnBPXnbM6FVDEO8E1kTdgHOFZOAXrktA=" crossorigin="anonymous" defer></script>
-	<script src="https://cdnjs.cloudflare.com/ajax/libs/js-url/2.3.0/url.min.js" integrity="sha256-HOZJz4x+1mn1Si84WT5XKXPtOlTytmZLnMb6n1v4+5Q=" crossorigin="anonymous" defer></script>
-    <script src="https://cdnjs.cloudflare.com/ajax/libs/1000hz-bootstrap-validator/0.11.8/validator.min.js" integrity="sha256-LHeY7YoYJ0SSXbCx7sR14Pqna+52moaH3bhv0Mjzd/M=" crossorigin="anonymous" defer></script>
-    <%- include build/cover-scripts %>
-	<% } else { %>
-    <%- include build/cover-pack-scripts %>
-	<% } %>
+    <%- include index/header %>
+    <%- include index/body %>
+    <%- include index/footer %>
+    <%- include index/foot %>
 </body>
 
 </html>
diff --git a/public/views/index/body.ejs b/public/views/index/body.ejs
new file mode 100644
index 0000000..584d67e
--- /dev/null
+++ b/public/views/index/body.ejs
@@ -0,0 +1,176 @@
+<div class="site-wrapper">
+    <div class="site-wrapper-inner">
+        <div class="cover-container">
+
+            <div class="masthead clearfix">
+                <div class="inner">
+                    <h3 class="masthead-brand"></h3>
+                    <nav>
+                        <ul class="nav masthead-nav">
+                            <li class="ui-home<% if(!signin) { %> active<% } %>"><a href="#"><%= __('Intro') %></a>
+                            </li>
+                            <li class="ui-history<% if(signin) { %> active<% } %>"><a href="#"><%= __('History') %></a>
+                            </li>
+                            <div class="ui-signin" style="float: right; margin-top: 8px;<% if(signin) { %> display: none;<% } %>">
+                                <% if(allowAnonymous) { %>
+                                <a type="button" href="<%- url %>/new" class="btn btn-sm btn-link"><i class="fa fa-plus"></i> <%= __('New guest note') %></a>
+                                <% } %>
+                                <% if(facebook || twitter || github || gitlab || dropbox || google || ldap || email) { %>
+                                <button class="btn btn-sm btn-success ui-signin" data-toggle="modal" data-target=".signin-modal"><%= __('Sign In') %></button>
+                                <% } %>
+                            </div>
+                            <div class="ui-signout" style="float: right; margin-top: 8px;<% if(!signin) { %> display: none;<% } %>">
+                                <a type="button" href="<%- url %>/new" class="btn btn-sm btn-link"><i class="fa fa-plus"></i> <%= __('New note') %></a>
+                                <span class="ui-profile dropdown pull-right">
+                                    <button id="profileLabel" class="btn btn-sm btn-link ui-profile-label" style="padding-right: 0;" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
+                                        <img class="ui-avatar" width="20" height="20"><span class="hidden-xs hidden-sm">&ensp;<span class="ui-name"></span></span>&ensp;<i class="fa fa-caret-down"></i>
+                                    </button>
+                                    <ul class="dropdown-menu" aria-labelledby="profileLabel">
+                                        <li><a href="<%- url %>/features"><i class="fa fa-dot-circle-o fa-fw"></i> <%= __('Features') %></a></li>
+                                        <li><a href="<%- url %>/logout"><i class="fa fa-sign-out fa-fw"></i> <%= __('Sign Out') %></a></li>
+                                    </ul>
+                                </span>
+                            </div>
+                        </ul>
+                    </nav>
+                </div>
+            </div>
+
+            <div id="home" class="section"<% if(signin) { %> style="display:none;"<% } %>>
+                <div class="inner cover">
+                    <h1 class="cover-heading"><i class="fa fa-file-text"></i> HackMD</h1>
+                    <p class="lead">
+                        <%= __('Best way to write and share your knowledge in markdown.') %>
+                    </p>
+                    <% if (infoMessage && infoMessage.length > 0) { %>
+                    <div class="alert alert-info" style="max-width: 400px; margin: 0 auto;"><%= infoMessage %></div>
+                    <% } %>
+                    <% if (errorMessage && errorMessage.length > 0) { %>
+                    <div class="alert alert-danger" style="max-width: 400px; margin: 0 auto;"><%= errorMessage %></div>
+                    <% } %>
+                    <% if(facebook || twitter || github || gitlab || dropbox || google || ldap || email) { %>
+                    <span class="ui-signin">
+                        <br>
+                        <a type="button" class="btn btn-lg btn-success ui-signin" data-toggle="modal" data-target=".signin-modal" style="min-width: 200px;"><%= __('Sign In') %></a>
+                    </span>
+                    <span class="ui-or"><%= __('or') %></span>
+                    <% } %>
+                    <span class="ui-signin">
+                        <a type="button" href="<%- url %>/features" class="btn btn-lg btn-primary" style="min-width: 200px;"><%= __('Explore all features') %></a>
+                        <br>
+                        <br>
+                        <img src="<%- url %>/screenshot.png" class="screenshot ui-signin">
+                    </span>
+                    <div class="lead row" style="width: 90%; margin: 0 auto;">
+                        <div class="col-md-4 inner">
+                            <a href="<%- url %>/features#share-notes">
+                                <i class="fa fa-bolt fa-3x"></i>
+                                <h4><%= __('Collaborate with URL') %></h4>
+                            </a>
+                        </div>
+                        <div class="col-md-4 inner">
+                            <a href="<%- url %>/features#mathjax">
+                                <i class="fa fa-bar-chart fa-3x"></i>
+                                <h4><%= __('Support charts and MathJax') %></h4>
+                            </a>
+                        </div>
+                        <div class="col-md-4 inner">
+                            <a href="<%- url %>/features#slide-mode">
+                                <i class="fa fa-tv fa-3x"></i>
+                                <h4><%= __('Support slide mode') %></h4>
+                            </a>
+                        </div>
+                    </div>
+                </div>
+            </div>
+
+            <div id="history" class="section"<% if(!signin) { %> style="display:none;"<% } %>>
+                <div class="ui-signin"<% if(signin) { %> style="display:none;"<% } %>>
+                    <p><%= __('Below is the history from browser') %></p>
+                </div>
+                <br>
+                <form class="form-inline">
+                    <div class="form-group" style="vertical-align: bottom;">
+                        <input class="form-control ui-use-tags" placeholder="<%= __('Select tags...') %>" />
+                    </div>
+                    <div class="form-group">
+                        <input class="search form-control" placeholder="<%= __('Search keyword...') %>" />
+                    </div>
+                    <a href="#" class="sort btn btn-default" data-sort="text" title="<%= __('Sort by title') %>">
+                    <%= __('Title') %>
+                    </a>
+                    <a href="#" class="sort btn btn-default" data-sort="timestamp" title="<%= __('Sort by time') %>">
+                    <%= __('Time') %>
+                    </a>
+                    <span class="hidden-xs hidden-sm">
+                        <a href="#" class="btn btn-default ui-save-history" title="<%= __('Export history') %>"><i class="fa fa-save"></i></a>
+                        <span class="btn btn-default btn-file ui-open-history" title="<%= __('Import history') %>">
+                            <i class="fa fa-folder-open-o"></i><input type="file" />
+                        </span>
+                    <a href="#" class="btn btn-default ui-clear-history" title="<%= __('Clear history') %>" data-toggle="modal" data-target=".delete-modal"><i class="fa fa-trash-o"></i></a>
+                    </span>
+                    <a href="#" class="btn btn-default ui-refresh-history" title="<%= __('Refresh history') %>"><i class="fa fa-refresh"></i></a>
+                </form>
+                <h4 class="ui-nohistory" style="display:none;">
+                    <%= __('No history') %>
+                </h4>
+                <a href="#" class="btn btn-primary ui-import-from-browser" style="display:none;"><%= __('Import from browser') %></a>
+                <ul id="history-list" class="list">
+                </ul>
+                <ul class="pagination"></ul>
+            </div>
+
+            <div class="mastfoot">
+                <div class="inner">
+                    <h6 class="social-foot">
+                        <iframe src="//ghbtns.com/github-btn.html?user=hackmdio&repo=hackmd&type=star&count=true" frameborder="0" scrolling="0" width="104px" height="20px"></iframe>
+                    </h6>
+                    <p>
+                        &copy; 2017 <a href="https://www.facebook.com/hackmdio" target="_blank"><i class="fa fa-facebook-square"></i> HackMD</a> | <a href="<%- url %>/s/release-notes" target="_blank"><%= __('Releases') %></a>
+                    </p>
+                    <select class="ui-locale">
+                        <option value="en">English</option>
+                        <option value="zh">中文</option>
+                        <option value="fr">Français</option>
+                        <option value="de">Deutsch</option>
+                        <option value="ja">日本語</option>
+                        <option value="es">Español</option>
+                        <option value="el">Ελληνικά</option>
+                        <option value="pt">Português</option>
+                        <option value="it">italiano</option>
+                        <option value="tr">Türkçe</option>
+                        <option value="ru">Русский</option>
+                        <option value="nl">Nederlands</option>
+                        <option value="hr">hrvatski jezik</option>
+                        <option value="pl">język polski</option>
+                        <option value="uk">Українська</option>
+                        <option value="hi">हिन्दी</option>
+                        <option value="sv">svenska</option>
+                        <option value="eo">Esperanto</option>
+                    </select>
+                </div>
+            </div>
+        </div>
+    </div>
+</div>
+<!-- delete modal -->
+<div class="modal fade delete-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
+    <div class="modal-dialog modal-sm">
+        <div class="modal-content">
+            <div class="modal-header">
+                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
+                </button>
+                <h4 class="modal-title" id="myModalLabel"><%= __('Are you sure?') %></h4>
+            </div>
+            <div class="modal-body" style="color:black;">
+                <h5 class="ui-delete-modal-msg"></h5>
+                <strong class="ui-delete-modal-item"></strong>
+            </div>
+            <div class="modal-footer">
+                <button type="button" class="btn btn-default" data-dismiss="modal"><%= __('Cancel') %></button>
+                <button type="button" class="btn btn-danger ui-delete-modal-confirm"><%= __('Yes, do it!') %></button>
+            </div>
+        </div>
+    </div>
+</div>
+<%- include ../shared/signin-modal %>
\ No newline at end of file
diff --git a/public/views/index/foot.ejs b/public/views/index/foot.ejs
new file mode 100644
index 0000000..293c669
--- /dev/null
+++ b/public/views/index/foot.ejs
@@ -0,0 +1,13 @@
+<% if(useCDN) { %>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.1.1/jquery.min.js" integrity="sha256-hVVnYaiADRTO2PzUGmuLJr8BLUSjGIZsDYGmIJLv2b8=" crossorigin="anonymous"></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.4.0/velocity.min.js" integrity="sha256-bhm0lgEt6ITaZCDzZpkr/VXVrLa5RP4u9v2AYsbzSUk=" crossorigin="anonymous" defer></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js" integrity="sha256-U5ZEeKfGNOja007MMD3YBI0A3OSZOQbeG6z2f2Y0hu8=" crossorigin="anonymous" defer></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/list.pagination.js/0.1.1/list.pagination.min.js" integrity="sha256-WwTza96H3BgcQTfEfxX7MFaFc/dZA0QrPRKDRLdFHJo=" crossorigin="anonymous" defer></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/select2/3.5.2/select2.min.js" integrity="sha256-HzzZFiY4t0PIv02Tm8/R3CVvLpcjHhO1z/YAUCp4oQ4=" crossorigin="anonymous" defer></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment-with-locales.min.js" integrity="sha256-vvT7Ok9u6GbfnBPXnbM6FVDEO8E1kTdgHOFZOAXrktA=" crossorigin="anonymous" defer></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/js-url/2.3.0/url.min.js" integrity="sha256-HOZJz4x+1mn1Si84WT5XKXPtOlTytmZLnMb6n1v4+5Q=" crossorigin="anonymous" defer></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/1000hz-bootstrap-validator/0.11.8/validator.min.js" integrity="sha256-LHeY7YoYJ0SSXbCx7sR14Pqna+52moaH3bhv0Mjzd/M=" crossorigin="anonymous" defer></script>
+<%- include ../build/cover-scripts %>
+<% } else { %>
+<%- include ../build/cover-pack-scripts %>
+<% } %>
\ No newline at end of file
diff --git a/public/views/index/footer.ejs b/public/views/index/footer.ejs
new file mode 100644
index 0000000..e69de29
diff --git a/public/views/index/head.ejs b/public/views/index/head.ejs
new file mode 100644
index 0000000..bbd1456
--- /dev/null
+++ b/public/views/index/head.ejs
@@ -0,0 +1,22 @@
+<meta charset="utf-8">
+<meta http-equiv="X-UA-Compatible" content="IE=edge">
+<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
+<meta name="apple-mobile-web-app-capable" content="yes">
+<meta name="apple-mobile-web-app-status-bar-style" content="black">
+<meta name="mobile-web-app-capable" content="yes">
+<meta name="description" content="<%= __('Best way to write and share your knowledge in markdown.') %>">
+<meta name="keywords" content="Collaborative, Markdown, Notes">
+<title>HackMD - <%= __('Collaborative markdown notes') %></title>
+<link rel="icon" type="image/png" href="<%- url %>/favicon.png">
+<link rel="apple-touch-icon" href="<%- url %>/apple-touch-icon.png">
+<% if(useCDN) { %>
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" integrity="sha256-916EbMg70RQy9LHiGkXzG8hSg9EdNy97GazNG/aiY1w=" crossorigin="anonymous" />
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css" integrity="sha256-eZrrJcwDc/3uDhsdt61sL2oOBY362qM3lon1gyExkL0=" crossorigin="anonymous" />
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-social/4.9.0/bootstrap-social.min.css" integrity="sha256-02JtFTurpwBjQJ6q13iJe82/NF0RbZlJroDegK5g87Y=" crossorigin="anonymous" />
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/select2/3.5.4/select2.min.css" integrity="sha256-ijlUKKj3hJCiiT2HWo1kqkI79NTEYpzOsw5Rs3k42dI=" crossorigin="anonymous" />
+<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/select2/3.5.4/select2-bootstrap.min.css" integrity="sha256-NAWFcNIZdH+TS1xpWujF/EB/Y8gwBbEOCoaK/eqaer8=" crossorigin="anonymous" />
+<%- include ../build/cover-header %>
+<% } else { %>
+<%- include ../build/cover-pack-header %>
+<% } %>
+<%- include ../shared/polyfill %>
\ No newline at end of file
diff --git a/public/views/index/header.ejs b/public/views/index/header.ejs
new file mode 100644
index 0000000..e69de29
diff --git a/public/views/pretty.ejs b/public/views/pretty.ejs
index ced65ed..64b65c7 100644
--- a/public/views/pretty.ejs
+++ b/public/views/pretty.ejs
@@ -28,7 +28,7 @@
     <link rel="stylesheet" href='<%- url %>/build/emojify.js/dist/css/basic/emojify.min.css'>
     <%- include build/pretty-pack-header %>
 	<% } %>
-    <%- include polyfill %>
+    <%- include shared/polyfill %>
 </head>
 
 <body style="display:none;">
@@ -66,7 +66,7 @@
     <div id="ui-toc-affix" class="ui-affix-toc ui-toc-dropdown unselectable hidden-print" data-spy="affix" style="display:none;"></div>
     <% if(typeof disqus !== 'undefined' && disqus) { %>
     <div class="container-fluid" style="max-width: 758px; margin-bottom: 40px;">
-        <%- include disqus %>
+        <%- include shared/disqus %>
     </div>
     <% } %>
 </body>
@@ -84,16 +84,16 @@
 <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js" integrity="sha256-yYfngbEKv4RENfGDvNUqJTqGFcKf31NJEe9OTnnMH3Y=" crossorigin="anonymous" defer></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/config/TeX-AMS-MML_HTMLorMML.js" integrity="sha256-immzXfCGLhnx3Zfi9F/dUcqxEM8K3o3oTFy9Bh6HCwg=" crossorigin="anonymous" defer></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment-with-locales.min.js" integrity="sha256-vvT7Ok9u6GbfnBPXnbM6FVDEO8E1kTdgHOFZOAXrktA=" crossorigin="anonymous" defer></script>
-<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/6.0.0/mermaid.min.js" integrity="sha256-Yabf6Mj1TPKd6h4F6z5xRR1/2son0Wg8NhvjYnhcQcY=" crossorigin="anonymous" defer></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/7.0.0/mermaid.min.js" integrity="sha256-1uR+pqxH5fN/rOZcZTb9c5+bR3OIYEKzu2sI11Dnj9A=" crossorigin="anonymous" defer></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/emojify.js/1.1.0/js/emojify.min.js" integrity="sha256-VAB5tAlKBvgaxw8oJ1crWMVbdmBVl4mP/2M8MNRl+4E=" crossorigin="anonymous" defer></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.6/handlebars.min.js" integrity="sha256-1O3BtOwnPyyRzOszK6P+gqaRoXHV6JXj8HkjZmPYhCI=" crossorigin="anonymous" defer></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.9.0/highlight.min.js" integrity="sha256-KbfTjB0WZ8vvXngdpJGY3Yp3xKk+tttbqClO11anCIU=" crossorigin="anonymous" defer></script>
 <script src="https://cdnjs.cloudflare.com/ajax/libs/gist-embed/2.6.0/gist-embed.min.js" integrity="sha256-KyF2D6xPIJUW5sUDSs93vWyZm+1RzIpKCexxElmxl8g=" crossorigin="anonymous" defer></script>
-<script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/1.4.1/viz.js" integrity="sha256-U0a9HpXT7zG0N3tVzo58B5S+QXUxo4FdBIjrBMYrxZI=" crossorigin="anonymous" defer></script>
+<script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/1.7.0/viz.js" integrity="sha256-8t+rndrF+TU4JtelmOH1lDHTMe2ovhO2UbzDArp5lY8=" crossorigin="anonymous" defer></script>
 <%- include build/pretty-scripts %>
 <% } else { %>
 <script src="<%- url %>/build/MathJax/MathJax.js" defer></script>
 <script src="<%- url %>/build/MathJax/config/TeX-AMS-MML_HTMLorMML.js" defer></script>
 <%- include build/pretty-pack-scripts %>
 <% } %>
-<%- include ga %>
+<%- include shared/ga %>
diff --git a/public/views/disqus.ejs b/public/views/shared/disqus.ejs
similarity index 100%
rename from public/views/disqus.ejs
rename to public/views/shared/disqus.ejs
diff --git a/public/views/ga.ejs b/public/views/shared/ga.ejs
similarity index 100%
rename from public/views/ga.ejs
rename to public/views/shared/ga.ejs
diff --git a/public/views/help-modal.ejs b/public/views/shared/help-modal.ejs
similarity index 100%
rename from public/views/help-modal.ejs
rename to public/views/shared/help-modal.ejs
diff --git a/public/views/polyfill.ejs b/public/views/shared/polyfill.ejs
similarity index 100%
rename from public/views/polyfill.ejs
rename to public/views/shared/polyfill.ejs
diff --git a/public/views/refresh-modal.ejs b/public/views/shared/refresh-modal.ejs
similarity index 100%
rename from public/views/refresh-modal.ejs
rename to public/views/shared/refresh-modal.ejs
diff --git a/public/views/revision-modal.ejs b/public/views/shared/revision-modal.ejs
similarity index 100%
rename from public/views/revision-modal.ejs
rename to public/views/shared/revision-modal.ejs
diff --git a/public/views/signin-modal.ejs b/public/views/shared/signin-modal.ejs
similarity index 100%
rename from public/views/signin-modal.ejs
rename to public/views/shared/signin-modal.ejs
diff --git a/public/views/slide.ejs b/public/views/slide.ejs
index b0323a0..a3e2d1d 100644
--- a/public/views/slide.ejs
+++ b/public/views/slide.ejs
@@ -45,7 +45,7 @@
 			document.getElementsByTagName( 'head' )[0].appendChild( link );
 		</script>
 
-        <%- include polyfill %>
+        <%- include shared/polyfill %>
     </head>
     <body>
         <div class="container">
@@ -79,7 +79,7 @@
                 </div>
                 <% if(typeof disqus !== 'undefined' && disqus) { %>
                 <div style="margin-top: 25px; margin-bottom: 15px;">
-                    <%- include disqus %>
+                    <%- include shared/disqus %>
                 </div>
                 <% } %>
             </div>
@@ -98,12 +98,12 @@
         <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/MathJax.js" integrity="sha256-yYfngbEKv4RENfGDvNUqJTqGFcKf31NJEe9OTnnMH3Y=" crossorigin="anonymous" defer></script>
         <script src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.0/config/TeX-AMS-MML_HTMLorMML.js" integrity="sha256-immzXfCGLhnx3Zfi9F/dUcqxEM8K3o3oTFy9Bh6HCwg=" crossorigin="anonymous" defer></script>
         <script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.17.1/moment-with-locales.min.js" integrity="sha256-vvT7Ok9u6GbfnBPXnbM6FVDEO8E1kTdgHOFZOAXrktA=" crossorigin="anonymous" defer></script>
-        <script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/6.0.0/mermaid.min.js" integrity="sha256-Yabf6Mj1TPKd6h4F6z5xRR1/2son0Wg8NhvjYnhcQcY=" crossorigin="anonymous" defer></script>
+        <script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/7.0.0/mermaid.min.js" integrity="sha256-1uR+pqxH5fN/rOZcZTb9c5+bR3OIYEKzu2sI11Dnj9A=" crossorigin="anonymous" defer></script>
         <script src="https://cdnjs.cloudflare.com/ajax/libs/emojify.js/1.1.0/js/emojify.min.js" integrity="sha256-VAB5tAlKBvgaxw8oJ1crWMVbdmBVl4mP/2M8MNRl+4E=" crossorigin="anonymous" defer></script>
         <script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.6/handlebars.min.js" integrity="sha256-1O3BtOwnPyyRzOszK6P+gqaRoXHV6JXj8HkjZmPYhCI=" crossorigin="anonymous" defer></script>
         <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/9.9.0/highlight.min.js" integrity="sha256-KbfTjB0WZ8vvXngdpJGY3Yp3xKk+tttbqClO11anCIU=" crossorigin="anonymous" defer></script>
         <script src="https://cdnjs.cloudflare.com/ajax/libs/gist-embed/2.6.0/gist-embed.min.js" integrity="sha256-KyF2D6xPIJUW5sUDSs93vWyZm+1RzIpKCexxElmxl8g=" crossorigin="anonymous" defer></script>
-        <script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/1.4.1/viz.js" integrity="sha256-U0a9HpXT7zG0N3tVzo58B5S+QXUxo4FdBIjrBMYrxZI=" crossorigin="anonymous" defer></script>
+        <script src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/1.7.0/viz.js" integrity="sha256-8t+rndrF+TU4JtelmOH1lDHTMe2ovhO2UbzDArp5lY8=" crossorigin="anonymous" defer></script>
         <%- include build/slide-scripts %>
         <% } else { %>
         <script src="<%- url %>/build/MathJax/MathJax.js" defer></script>
@@ -113,4 +113,4 @@
     </body>
 </html>
 
-<%- include ga %>
+<%- include shared/ga %>
diff --git a/webpack.production.js b/webpack.production.js
index b28c34a..7c690d2 100644
--- a/webpack.production.js
+++ b/webpack.production.js
@@ -3,6 +3,7 @@ var webpack = require('webpack');
 var path = require('path');
 var ExtractTextPlugin = require("extract-text-webpack-plugin");
 var OptimizeCssAssetsPlugin = require('optimize-css-assets-webpack-plugin');
+var ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');
 
 module.exports = [Object.assign({}, baseConfig, {
     plugins: baseConfig.plugins.concat([
@@ -11,12 +12,14 @@ module.exports = [Object.assign({}, baseConfig, {
                 'NODE_ENV': JSON.stringify('production')
             }
         }),
-        new webpack.optimize.UglifyJsPlugin({
-            compress: {
-                warnings: false
-            },
-            mangle: false,
-            sourceMap: false
+        new ParallelUglifyPlugin({
+            uglifyJS: {
+                compress: {
+                    warnings: false
+                },
+                mangle: false,
+                sourceMap: false
+            }
         }),
         new ExtractTextPlugin("[name].[hash].css")
     ]),
diff --git a/webpackBaseConfig.js b/webpackBaseConfig.js
index 496afce..419149c 100644
--- a/webpackBaseConfig.js
+++ b/webpackBaseConfig.js
@@ -158,6 +158,7 @@ module.exports = {
             "bootstrap"
         ],
         cover: [
+            "babel-polyfill",
             path.join(__dirname, 'public/js/cover.js')
         ],
         "cover-styles-pack": [
@@ -168,6 +169,7 @@ module.exports = {
             path.join(__dirname, 'node_modules/select2/select2-bootstrap.css'),
         ],
         "cover-pack": [
+            "babel-polyfill",
             "bootstrap-validator",
             "script!listPagnation",
             "expose?select2!select2",
@@ -176,6 +178,7 @@ module.exports = {
             path.join(__dirname, 'public/js/cover.js')
         ],
         index: [
+            "babel-polyfill",
             "script!jquery-ui-resizable",
             "script!js-url",
             "expose?filterXSS!xss",
@@ -221,6 +224,7 @@ module.exports = {
             path.join(__dirname, 'node_modules/octicons/octicons/octicons.css')
         ],
         "index-pack": [
+            "babel-polyfill",
             "expose?Spinner!spin.js",
             "script!jquery-ui-resizable",
             "bootstrap-validator",
@@ -251,6 +255,7 @@ module.exports = {
             path.join(__dirname, 'public/js/index.js')
         ],
         pretty: [
+            "babel-polyfill",
             "expose?filterXSS!xss",
             "flowchart.js",
             "js-sequence-diagrams",
@@ -270,6 +275,7 @@ module.exports = {
             path.join(__dirname, 'node_modules/octicons/octicons/octicons.css')
         ],
         "pretty-pack": [
+            "babel-polyfill",
             "expose?jsyaml!js-yaml",
             "script!mermaid",
             "expose?moment!moment",
@@ -285,6 +291,7 @@ module.exports = {
             path.join(__dirname, 'public/js/pretty.js')
         ],
         slide: [
+            "babel-polyfill",
             "bootstrap-tooltip",
             "expose?filterXSS!xss",
             "flowchart.js",
@@ -304,6 +311,7 @@ module.exports = {
             path.join(__dirname, 'node_modules/octicons/octicons/octicons.css')
         ],
         "slide-pack": [
+            "babel-polyfill",
             "expose?jQuery!expose?$!jquery",
             "velocity-animate",
             "imports?$=jquery!jquery-mousewheel",
@@ -372,6 +380,10 @@ module.exports = {
         loaders: [{
             test: /\.json$/,
             loader: 'json-loader'
+        }, {
+            test: /\.js$/,
+            loader: 'babel',
+            exclude: [/node_modules/, /public\/vendor/]
         }, {
             test: /\.css$/,
             loader: ExtractTextPlugin.extract('style-loader', 'css-loader')