diff options
40 files changed, 10539 insertions, 10844 deletions
@@ -1,654 +1,656 @@ -//app -//external modules -var express = require('express'); -var toobusy = require('toobusy-js'); -var ejs = require('ejs'); -var passport = require('passport'); -var methodOverride = require('method-override'); -var cookieParser = require('cookie-parser'); -var bodyParser = require('body-parser'); +// app +// external modules +var express = require('express') +var toobusy = require('toobusy-js') +var ejs = require('ejs') +var passport = require('passport') +var methodOverride = require('method-override') +var cookieParser = require('cookie-parser') +var bodyParser = require('body-parser') var compression = require('compression') -var session = require('express-session'); -var SequelizeStore = require('connect-session-sequelize')(session.Store); -var fs = require('fs'); -var url = require('url'); -var path = require('path'); -var imgur = require('imgur'); -var formidable = require('formidable'); -var morgan = require('morgan'); -var passportSocketIo = require("passport.socketio"); -var helmet = require('helmet'); -var i18n = require('i18n'); -var flash = require('connect-flash'); -var validator = require('validator'); - -//core -var config = require("./lib/config.js"); -var logger = require("./lib/logger.js"); -var auth = require("./lib/auth.js"); -var response = require("./lib/response.js"); -var models = require("./lib/models"); - -//server setup +var session = require('express-session') +var SequelizeStore = require('connect-session-sequelize')(session.Store) +var fs = require('fs') +var url = require('url') +var path = require('path') +var imgur = require('imgur') +var formidable = require('formidable') +var morgan = require('morgan') +var passportSocketIo = require('passport.socketio') +var helmet = require('helmet') +var i18n = require('i18n') +var flash = require('connect-flash') +var validator = require('validator') + +// core +var config = require('./lib/config.js') +var logger = require('./lib/logger.js') +var auth = require('./lib/auth.js') +var response = require('./lib/response.js') +var models = require('./lib/models') + +// server setup +var app = express() +var server = null if (config.usessl) { - var ca = (function () { - var i, len, results; - results = []; - for (i = 0, len = config.sslcapath.length; i < len; i++) { - results.push(fs.readFileSync(config.sslcapath[i], 'utf8')); - } - return results; - })(); - var options = { - key: fs.readFileSync(config.sslkeypath, 'utf8'), - cert: fs.readFileSync(config.sslcertpath, 'utf8'), - ca: ca, - dhparam: fs.readFileSync(config.dhparampath, 'utf8'), - requestCert: false, - rejectUnauthorized: false - }; - var app = express(); - var server = require('https').createServer(options, app); + var ca = (function () { + var i, len, results + results = [] + for (i = 0, len = config.sslcapath.length; i < len; i++) { + results.push(fs.readFileSync(config.sslcapath[i], 'utf8')) + } + return results + })() + var options = { + key: fs.readFileSync(config.sslkeypath, 'utf8'), + cert: fs.readFileSync(config.sslcertpath, 'utf8'), + ca: ca, + dhparam: fs.readFileSync(config.dhparampath, 'utf8'), + requestCert: false, + rejectUnauthorized: false + } + server = require('https').createServer(options, app) } else { - var app = express(); - var server = require('http').createServer(app); + server = require('http').createServer(app) } -//logger +// logger app.use(morgan('combined', { - "stream": logger.stream -})); + 'stream': logger.stream +})) -//socket io -var io = require('socket.io')(server); +// socket io +var io = require('socket.io')(server) io.engine.ws = new (require('uws').Server)({ - noServer: true, - perMessageDeflate: false -}); + noServer: true, + perMessageDeflate: false +}) -//others -var realtime = require("./lib/realtime.js"); +// others +var realtime = require('./lib/realtime.js') -//assign socket io to realtime -realtime.io = io; +// assign socket io to realtime +realtime.io = io -//methodOverride -app.use(methodOverride('_method')); - -// create application/json parser -var jsonParser = bodyParser.json({ - limit: 1024 * 1024 * 10 // 10 mb -}); +// methodOverride +app.use(methodOverride('_method')) // create application/x-www-form-urlencoded parser var urlencodedParser = bodyParser.urlencoded({ - extended: false, - limit: 1024 * 1024 * 10 // 10 mb -}); + extended: false, + limit: 1024 * 1024 * 10 // 10 mb +}) -//session store +// session store var sessionStore = new SequelizeStore({ - db: models.sequelize -}); + db: models.sequelize +}) -//compression -app.use(compression()); +// compression +app.use(compression()) // use hsts to tell https users stick to this app.use(helmet.hsts({ - maxAge: 31536000 * 1000, // 365 days - includeSubdomains: true, - preload: true -})); + maxAge: 31536000 * 1000, // 365 days + includeSubdomains: true, + preload: true +})) i18n.configure({ - locales: ['en', 'zh', 'fr', 'de', 'ja', 'es', 'el', 'pt', 'it', 'tr', 'ru', 'nl', 'hr', 'pl', 'uk', 'hi', 'sv', 'eo'], - cookie: 'locale', - directory: __dirname + '/locales' -}); + locales: ['en', 'zh', 'fr', 'de', 'ja', 'es', 'el', 'pt', 'it', 'tr', 'ru', 'nl', 'hr', 'pl', 'uk', 'hi', 'sv', 'eo'], + cookie: 'locale', + directory: path.join(__dirname, '/locales') +}) -app.use(cookieParser()); +app.use(cookieParser()) -app.use(i18n.init); +app.use(i18n.init) // routes without sessions // static files -app.use('/', express.static(__dirname + '/public', { maxAge: config.staticcachetime })); +app.use('/', express.static(path.join(__dirname, '/public'), { maxAge: config.staticcachetime })) -//session +// session app.use(session({ - name: config.sessionname, - secret: config.sessionsecret, - resave: false, //don't save session if unmodified - saveUninitialized: true, //always create session to ensure the origin - rolling: true, // reset maxAge on every response - cookie: { - maxAge: config.sessionlife - }, - store: sessionStore -})); + name: config.sessionname, + secret: config.sessionsecret, + resave: false, // don't save session if unmodified + saveUninitialized: true, // always create session to ensure the origin + rolling: true, // reset maxAge on every response + cookie: { + maxAge: config.sessionlife + }, + store: sessionStore +})) // session resumption -var tlsSessionStore = {}; +var tlsSessionStore = {} server.on('newSession', function (id, data, cb) { - tlsSessionStore[id.toString('hex')] = data; - cb(); -}); + tlsSessionStore[id.toString('hex')] = data + cb() +}) server.on('resumeSession', function (id, cb) { - cb(null, tlsSessionStore[id.toString('hex')] || null); -}); + cb(null, tlsSessionStore[id.toString('hex')] || null) +}) -//middleware which blocks requests when we're too busy +// middleware which blocks requests when we're too busy app.use(function (req, res, next) { - if (toobusy()) { - response.errorServiceUnavailable(res); - } else { - next(); - } -}); + if (toobusy()) { + response.errorServiceUnavailable(res) + } else { + next() + } +}) -app.use(flash()); +app.use(flash()) -//passport -app.use(passport.initialize()); -app.use(passport.session()); +// passport +app.use(passport.initialize()) +app.use(passport.session()) +auth.registerAuthMethod() -//serialize and deserialize +// serialize and deserialize passport.serializeUser(function (user, done) { - logger.info('serializeUser: ' + user.id); - return done(null, user.id); -}); + logger.info('serializeUser: ' + user.id) + return done(null, user.id) +}) passport.deserializeUser(function (id, done) { - models.User.findOne({ - where: { - id: id - } - }).then(function (user) { - logger.info('deserializeUser: ' + user.id); - return done(null, user); - }).catch(function (err) { - logger.error(err); - return done(err, null); - }); -}); + models.User.findOne({ + where: { + id: id + } + }).then(function (user) { + logger.info('deserializeUser: ' + user.id) + return done(null, user) + }).catch(function (err) { + logger.error(err) + return done(err, null) + }) +}) // check uri is valid before going further -app.use(function(req, res, next) { - try { - decodeURIComponent(req.path); - } catch (err) { - logger.error(err); - return response.errorBadRequest(res); - } - next(); -}); +app.use(function (req, res, next) { + try { + decodeURIComponent(req.path) + } catch (err) { + logger.error(err) + return response.errorBadRequest(res) + } + next() +}) // redirect url without trailing slashes -app.use(function(req, res, next) { - if ("GET" == req.method && req.path.substr(-1) == '/' && req.path.length > 1) { - var query = req.url.slice(req.path.length); - var urlpath = req.path.slice(0, -1); - var serverurl = config.serverurl; - if (config.urlpath) serverurl = serverurl.slice(0, -(config.urlpath.length + 1)); - res.redirect(301, serverurl + urlpath + query); - } else { - next(); - } -}); +app.use(function (req, res, next) { + if (req.method === 'GET' && req.path.substr(-1) === '/' && req.path.length > 1) { + var query = req.url.slice(req.path.length) + var urlpath = req.path.slice(0, -1) + var serverurl = config.serverurl + if (config.urlpath) serverurl = serverurl.slice(0, -(config.urlpath.length + 1)) + res.redirect(301, serverurl + urlpath + query) + } else { + next() + } +}) // routes need sessions -//template files -app.set('views', __dirname + '/public/views'); -//set render engine -app.engine('ejs', ejs.renderFile); -//set view engine -app.set('view engine', 'ejs'); -//get index -app.get("/", response.showIndex); -//get 403 forbidden -app.get("/403", function (req, res) { - response.errorForbidden(res); -}); -//get 404 not found -app.get("/404", function (req, res) { - response.errorNotFound(res); -}); -//get 500 internal error -app.get("/500", function (req, res) { - response.errorInternalError(res); -}); -//get status -app.get("/status", function (req, res, next) { - realtime.getStatus(function (data) { - res.set({ - 'Cache-Control': 'private', // only cache by client - 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling - }); - res.send(data); - }); -}); -//get status -app.get("/temp", function (req, res) { - var host = req.get('host'); - if (config.alloworigin.indexOf(host) == -1) - response.errorForbidden(res); - else { - var tempid = req.query.tempid; - if (!tempid) - response.errorForbidden(res); - else { - models.Temp.findOne({ - where: { - id: tempid - } - }).then(function (temp) { - if (!temp) - response.errorNotFound(res); - else { - res.header("Access-Control-Allow-Origin", "*"); - res.send({ - temp: temp.data - }); - temp.destroy().catch(function (err) { - if (err) - logger.error('remove temp failed: ' + err); - }); - } - }).catch(function (err) { - logger.error(err); - return response.errorInternalError(res); - }); +// template files +app.set('views', path.join(__dirname, '/public/views')) +// set render engine +app.engine('ejs', ejs.renderFile) +// set view engine +app.set('view engine', 'ejs') +// get index +app.get('/', response.showIndex) +// get 403 forbidden +app.get('/403', function (req, res) { + response.errorForbidden(res) +}) +// get 404 not found +app.get('/404', function (req, res) { + response.errorNotFound(res) +}) +// get 500 internal error +app.get('/500', function (req, res) { + response.errorInternalError(res) +}) +// get status +app.get('/status', function (req, res, next) { + realtime.getStatus(function (data) { + res.set({ + 'Cache-Control': 'private', // only cache by client + 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling + }) + res.send(data) + }) +}) +// get status +app.get('/temp', function (req, res) { + var host = req.get('host') + if (config.alloworigin.indexOf(host) === -1) { + response.errorForbidden(res) + } else { + var tempid = req.query.tempid + if (!tempid) { + response.errorForbidden(res) + } else { + models.Temp.findOne({ + where: { + id: tempid } + }).then(function (temp) { + if (!temp) { + response.errorNotFound(res) + } else { + res.header('Access-Control-Allow-Origin', '*') + res.send({ + temp: temp.data + }) + temp.destroy().catch(function (err) { + if (err) { + logger.error('remove temp failed: ' + err) + } + }) + } + }).catch(function (err) { + logger.error(err) + return response.errorInternalError(res) + }) } -}); -//post status -app.post("/temp", urlencodedParser, function (req, res) { - var host = req.get('host'); - if (config.alloworigin.indexOf(host) == -1) - response.errorForbidden(res); - else { - var data = req.body.data; - if (!data) - response.errorForbidden(res); - else { - if (config.debug) - logger.info('SERVER received temp from [' + host + ']: ' + req.body.data); - models.Temp.create({ - data: data - }).then(function (temp) { - if (temp) { - res.header("Access-Control-Allow-Origin", "*"); - res.send({ - status: 'ok', - id: temp.id - }); - } else - response.errorInternalError(res); - }).catch(function (err) { - logger.error(err); - return response.errorInternalError(res); - }); + } +}) +// post status +app.post('/temp', urlencodedParser, function (req, res) { + var host = req.get('host') + if (config.alloworigin.indexOf(host) === -1) { + response.errorForbidden(res) + } else { + var data = req.body.data + if (!data) { + response.errorForbidden(res) + } else { + if (config.debug) { + logger.info('SERVER received temp from [' + host + ']: ' + req.body.data) + } + models.Temp.create({ + data: data + }).then(function (temp) { + if (temp) { + res.header('Access-Control-Allow-Origin', '*') + res.send({ + status: 'ok', + id: temp.id + }) + } else { + response.errorInternalError(res) } + }).catch(function (err) { + logger.error(err) + return response.errorInternalError(res) + }) } -}); + } +}) -function setReturnToFromReferer(req) { - var referer = req.get('referer'); - if (!req.session) req.session = {}; - req.session.returnTo = referer; +function setReturnToFromReferer (req) { + var referer = req.get('referer') + if (!req.session) req.session = {} + req.session.returnTo = referer } -//facebook auth +// facebook auth if (config.facebook) { - app.get('/auth/facebook', function (req, res, next) { - setReturnToFromReferer(req); - passport.authenticate('facebook')(req, res, next); - }); - //facebook auth callback - app.get('/auth/facebook/callback', + app.get('/auth/facebook', function (req, res, next) { + setReturnToFromReferer(req) + passport.authenticate('facebook')(req, res, next) + }) + // facebook auth callback + app.get('/auth/facebook/callback', passport.authenticate('facebook', { - successReturnToOrRedirect: config.serverurl + '/', - failureRedirect: config.serverurl + '/' - })); + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + })) } -//twitter auth +// twitter auth if (config.twitter) { - app.get('/auth/twitter', function (req, res, next) { - setReturnToFromReferer(req); - passport.authenticate('twitter')(req, res, next); - }); - //twitter auth callback - app.get('/auth/twitter/callback', + app.get('/auth/twitter', function (req, res, next) { + setReturnToFromReferer(req) + passport.authenticate('twitter')(req, res, next) + }) + // twitter auth callback + app.get('/auth/twitter/callback', passport.authenticate('twitter', { - successReturnToOrRedirect: config.serverurl + '/', - failureRedirect: config.serverurl + '/' - })); + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + })) } -//github auth +// github auth if (config.github) { - app.get('/auth/github', function (req, res, next) { - setReturnToFromReferer(req); - passport.authenticate('github')(req, res, next); - }); - //github auth callback - app.get('/auth/github/callback', + app.get('/auth/github', function (req, res, next) { + setReturnToFromReferer(req) + passport.authenticate('github')(req, res, next) + }) + // github auth callback + app.get('/auth/github/callback', passport.authenticate('github', { - successReturnToOrRedirect: config.serverurl + '/', - failureRedirect: config.serverurl + '/' - })); - //github callback actions - app.get('/auth/github/callback/:noteId/:action', response.githubActions); + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + })) + // github callback actions + app.get('/auth/github/callback/:noteId/:action', response.githubActions) } -//gitlab auth +// gitlab auth if (config.gitlab) { - app.get('/auth/gitlab', function (req, res, next) { - setReturnToFromReferer(req); - passport.authenticate('gitlab')(req, res, next); - }); - //gitlab auth callback - app.get('/auth/gitlab/callback', + app.get('/auth/gitlab', function (req, res, next) { + setReturnToFromReferer(req) + passport.authenticate('gitlab')(req, res, next) + }) + // gitlab auth callback + app.get('/auth/gitlab/callback', passport.authenticate('gitlab', { - successReturnToOrRedirect: config.serverurl + '/', - failureRedirect: config.serverurl + '/' - })); - //gitlab callback actions - app.get('/auth/gitlab/callback/:noteId/:action', response.gitlabActions); + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + })) + // gitlab callback actions + app.get('/auth/gitlab/callback/:noteId/:action', response.gitlabActions) } -//dropbox auth +// dropbox auth if (config.dropbox) { - app.get('/auth/dropbox', function (req, res, next) { - setReturnToFromReferer(req); - passport.authenticate('dropbox-oauth2')(req, res, next); - }); - //dropbox auth callback - app.get('/auth/dropbox/callback', + app.get('/auth/dropbox', function (req, res, next) { + setReturnToFromReferer(req) + passport.authenticate('dropbox-oauth2')(req, res, next) + }) + // dropbox auth callback + app.get('/auth/dropbox/callback', passport.authenticate('dropbox-oauth2', { - successReturnToOrRedirect: config.serverurl + '/', - failureRedirect: config.serverurl + '/' - })); + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + })) } -//google auth +// google auth if (config.google) { - app.get('/auth/google', function (req, res, next) { - setReturnToFromReferer(req); - passport.authenticate('google', { scope: ['profile'] })(req, res, next); - }); - //google auth callback - app.get('/auth/google/callback', + app.get('/auth/google', function (req, res, next) { + setReturnToFromReferer(req) + passport.authenticate('google', { scope: ['profile'] })(req, res, next) + }) + // google auth callback + app.get('/auth/google/callback', passport.authenticate('google', { - successReturnToOrRedirect: config.serverurl + '/', - failureRedirect: config.serverurl + '/' - })); + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/' + })) } // ldap auth if (config.ldap) { - app.post('/auth/ldap', urlencodedParser, function (req, res, next) { - if (!req.body.username || !req.body.password) return response.errorBadRequest(res); - setReturnToFromReferer(req); - passport.authenticate('ldapauth', { - successReturnToOrRedirect: config.serverurl + '/', - failureRedirect: config.serverurl + '/', - failureFlash: true - })(req, res, next); - }); + app.post('/auth/ldap', urlencodedParser, function (req, res, next) { + if (!req.body.username || !req.body.password) return response.errorBadRequest(res) + setReturnToFromReferer(req) + passport.authenticate('ldapauth', { + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/', + failureFlash: true + })(req, res, next) + }) } // email auth if (config.email) { - if (config.allowemailregister) - app.post('/register', urlencodedParser, function (req, res, next) { - if (!req.body.email || !req.body.password) return response.errorBadRequest(res); - if (!validator.isEmail(req.body.email)) return response.errorBadRequest(res); - models.User.findOrCreate({ - where: { - email: req.body.email - }, - defaults: { - password: req.body.password - } - }).spread(function (user, created) { - if (user) { - if (created) { - if (config.debug) logger.info('user registered: ' + user.id); - req.flash('info', "You've successfully registered, please signin."); - } else { - if (config.debug) logger.info('user found: ' + user.id); - req.flash('error', "This email has been used, please try another one."); - } - return res.redirect(config.serverurl + '/'); - } - req.flash('error', "Failed to register your account, please try again."); - return res.redirect(config.serverurl + '/'); - }).catch(function (err) { - logger.error('auth callback failed: ' + err); - return response.errorInternalError(res); - }); - }); - - app.post('/login', urlencodedParser, function (req, res, next) { - if (!req.body.email || !req.body.password) return response.errorBadRequest(res); - if (!validator.isEmail(req.body.email)) return response.errorBadRequest(res); - setReturnToFromReferer(req); - passport.authenticate('local', { - successReturnToOrRedirect: config.serverurl + '/', - failureRedirect: config.serverurl + '/', - failureFlash: 'Invalid email or password.' - })(req, res, next); - }); + if (config.allowemailregister) { + app.post('/register', urlencodedParser, function (req, res, next) { + if (!req.body.email || !req.body.password) return response.errorBadRequest(res) + if (!validator.isEmail(req.body.email)) return response.errorBadRequest(res) + models.User.findOrCreate({ + where: { + email: req.body.email + }, + defaults: { + password: req.body.password + } + }).spread(function (user, created) { + if (user) { + if (created) { + if (config.debug) { + logger.info('user registered: ' + user.id) + } + req.flash('info', "You've successfully registered, please signin.") + } else { + if (config.debug) { + logger.info('user found: ' + user.id) + } + req.flash('error', 'This email has been used, please try another one.') + } + return res.redirect(config.serverurl + '/') + } + req.flash('error', 'Failed to register your account, please try again.') + return res.redirect(config.serverurl + '/') + }).catch(function (err) { + logger.error('auth callback failed: ' + err) + return response.errorInternalError(res) + }) + }) + } + + app.post('/login', urlencodedParser, function (req, res, next) { + if (!req.body.email || !req.body.password) return response.errorBadRequest(res) + if (!validator.isEmail(req.body.email)) return response.errorBadRequest(res) + setReturnToFromReferer(req) + passport.authenticate('local', { + successReturnToOrRedirect: config.serverurl + '/', + failureRedirect: config.serverurl + '/', + failureFlash: 'Invalid email or password.' + })(req, res, next) + }) } -//logout +// logout app.get('/logout', function (req, res) { - if (config.debug && req.isAuthenticated()) - logger.info('user logout: ' + req.user.id); - req.logout(); - res.redirect(config.serverurl + '/'); -}); -var history = require("./lib/history.js"); -//get history -app.get('/history', history.historyGet); -//post history -app.post('/history', urlencodedParser, history.historyPost); -//post history by note id -app.post('/history/:noteId', urlencodedParser, history.historyPost); -//delete history -app.delete('/history', history.historyDelete); -//delete history by note id -app.delete('/history/:noteId', history.historyDelete); -//get me info + if (config.debug && req.isAuthenticated()) { logger.info('user logout: ' + req.user.id) } + req.logout() + res.redirect(config.serverurl + '/') +}) +var history = require('./lib/history.js') +// get history +app.get('/history', history.historyGet) +// post history +app.post('/history', urlencodedParser, history.historyPost) +// post history by note id +app.post('/history/:noteId', urlencodedParser, history.historyPost) +// delete history +app.delete('/history', history.historyDelete) +// delete history by note id +app.delete('/history/:noteId', history.historyDelete) +// get me info app.get('/me', function (req, res) { - if (req.isAuthenticated()) { - models.User.findOne({ - where: { - id: req.user.id - } - }).then(function (user) { - if (!user) - return response.errorNotFound(res); - var profile = models.User.getProfile(user); - res.send({ - status: 'ok', - id: req.user.id, - name: profile.name, - photo: profile.photo - }); - }).catch(function (err) { - logger.error('read me failed: ' + err); - return response.errorInternalError(res); - }); - } else { - res.send({ - status: 'forbidden' - }); - } -}); - -//upload image + if (req.isAuthenticated()) { + models.User.findOne({ + where: { + id: req.user.id + } + }).then(function (user) { + if (!user) { return response.errorNotFound(res) } + var profile = models.User.getProfile(user) + res.send({ + status: 'ok', + id: req.user.id, + name: profile.name, + photo: profile.photo + }) + }).catch(function (err) { + logger.error('read me failed: ' + err) + return response.errorInternalError(res) + }) + } else { + res.send({ + status: 'forbidden' + }) + } +}) + +// upload image app.post('/uploadimage', function (req, res) { - var form = new formidable.IncomingForm(); + var form = new formidable.IncomingForm() - form.keepExtensions = true; + form.keepExtensions = true - if (config.imageUploadType === 'filesystem') { - form.uploadDir = "public/uploads"; - } + if (config.imageUploadType === 'filesystem') { + form.uploadDir = 'public/uploads' + } - form.parse(req, function (err, fields, files) { - if (err || !files.image || !files.image.path) { - response.errorForbidden(res); - } else { - if (config.debug) - logger.info('SERVER received uploadimage: ' + JSON.stringify(files.image)); - - try { - switch (config.imageUploadType) { - case 'filesystem': - res.send({ - link: url.resolve(config.serverurl + '/', files.image.path.match(/^public\/(.+$)/)[1]) - }); - - break; - - case 's3': - var AWS = require('aws-sdk'); - var awsConfig = new AWS.Config(config.s3); - var s3 = new AWS.S3(awsConfig); - - fs.readFile(files.image.path, function (err, buffer) { - var params = { - Bucket: config.s3bucket, - Key: path.join('uploads', path.basename(files.image.path)), - Body: buffer - }; - - s3.putObject(params, function (err, data) { - if (err) { - logger.error(err); - res.status(500).end('upload image error'); - } else { - res.send({ - link: `https://s3-${config.s3.region}.amazonaws.com/${config.s3bucket}/${params.Key}` - }); - } - }); - - }); - - break; - - case 'imgur': - default: - imgur.setClientId(config.imgur.clientID); - imgur.uploadFile(files.image.path) - .then(function (json) { - if (config.debug) - logger.info('SERVER uploadimage success: ' + JSON.stringify(json)); - res.send({ - link: json.data.link.replace(/^http:\/\//i, 'https://') - }); - }) - .catch(function (err) { - logger.error(err); - return res.status(500).end('upload image error'); - }); - break; + form.parse(req, function (err, fields, files) { + if (err || !files.image || !files.image.path) { + response.errorForbidden(res) + } else { + if (config.debug) { logger.info('SERVER received uploadimage: ' + JSON.stringify(files.image)) } + + try { + switch (config.imageUploadType) { + case 'filesystem': + res.send({ + link: url.resolve(config.serverurl + '/', files.image.path.match(/^public\/(.+$)/)[1]) + }) + + break + + case 's3': + var AWS = require('aws-sdk') + var awsConfig = new AWS.Config(config.s3) + var s3 = new AWS.S3(awsConfig) + + fs.readFile(files.image.path, function (err, buffer) { + if (err) { + logger.error(err) + res.status(500).end('upload image error') + return + } + var params = { + Bucket: config.s3bucket, + Key: path.join('uploads', path.basename(files.image.path)), + Body: buffer + } + + s3.putObject(params, function (err, data) { + if (err) { + logger.error(err) + res.status(500).end('upload image error') + return } - } catch (err) { - logger.error(err); - return res.status(500).end('upload image error'); - } + res.send({ + link: `https://s3-${config.s3.region}.amazonaws.com/${config.s3bucket}/${params.Key}` + }) + }) + }) + break + case 'imgur': + default: + imgur.setClientId(config.imgur.clientID) + imgur.uploadFile(files.image.path) + .then(function (json) { + if (config.debug) { logger.info('SERVER uploadimage success: ' + JSON.stringify(json)) } + res.send({ + link: json.data.link.replace(/^http:\/\//i, 'https://') + }) + }) + .catch(function (err) { + logger.error(err) + return res.status(500).end('upload image error') + }) + break } - }); -}); -//get new note -app.get("/new", response.newNote); -//get publish note -app.get("/s/:shortid", response.showPublishNote); -//publish note actions -app.get("/s/:shortid/:action", response.publishNoteActions); -//get publish slide -app.get("/p/:shortid", response.showPublishSlide); -//publish slide actions -app.get("/p/:shortid/:action", response.publishSlideActions); -//get note by id -app.get("/:noteId", response.showNote); -//note actions -app.get("/:noteId/:action", response.noteActions); -//note actions with action id -app.get("/:noteId/:action/:actionId", response.noteActions); + } catch (err) { + logger.error(err) + return res.status(500).end('upload image error') + } + } + }) +}) +// get new note +app.get('/new', response.newNote) +// get publish note +app.get('/s/:shortid', response.showPublishNote) +// publish note actions +app.get('/s/:shortid/:action', response.publishNoteActions) +// get publish slide +app.get('/p/:shortid', response.showPublishSlide) +// publish slide actions +app.get('/p/:shortid/:action', response.publishSlideActions) +// get note by id +app.get('/:noteId', response.showNote) +// note actions +app.get('/:noteId/:action', response.noteActions) +// note actions with action id +app.get('/:noteId/:action/:actionId', response.noteActions) // response not found if no any route matches app.get('*', function (req, res) { - response.errorNotFound(res); -}); + response.errorNotFound(res) +}) -//socket.io secure -io.use(realtime.secure); -//socket.io auth +// socket.io secure +io.use(realtime.secure) +// socket.io auth io.use(passportSocketIo.authorize({ - cookieParser: cookieParser, - key: config.sessionname, - secret: config.sessionsecret, - store: sessionStore, - success: realtime.onAuthorizeSuccess, - fail: realtime.onAuthorizeFail -})); -//socket.io heartbeat -io.set('heartbeat interval', config.heartbeatinterval); -io.set('heartbeat timeout', config.heartbeattimeout); -//socket.io connection -io.sockets.on('connection', realtime.connection); - -//listen -function startListen() { - server.listen(config.port, function () { - var schema = config.usessl ? 'HTTPS' : 'HTTP'; - logger.info('%s Server listening at port %d', schema, config.port); - config.maintenance = false; - }); + cookieParser: cookieParser, + key: config.sessionname, + secret: config.sessionsecret, + store: sessionStore, + success: realtime.onAuthorizeSuccess, + fail: realtime.onAuthorizeFail +})) +// socket.io heartbeat +io.set('heartbeat interval', config.heartbeatinterval) +io.set('heartbeat timeout', config.heartbeattimeout) +// socket.io connection +io.sockets.on('connection', realtime.connection) + +// listen +function startListen () { + server.listen(config.port, function () { + var schema = config.usessl ? 'HTTPS' : 'HTTP' + logger.info('%s Server listening at port %d', schema, config.port) + config.maintenance = false + }) } // sync db then start listen models.sequelize.sync().then(function () { - // check if realtime is ready - if (realtime.isReady()) { - models.Revision.checkAllNotesRevision(function (err, notes) { - if (err) throw new Error(err); - if (!notes || notes.length <= 0) return startListen(); - }); - } else { - throw new Error('server still not ready after db synced'); - } -}); + // check if realtime is ready + if (realtime.isReady()) { + models.Revision.checkAllNotesRevision(function (err, notes) { + if (err) throw new Error(err) + if (!notes || notes.length <= 0) return startListen() + }) + } else { + throw new Error('server still not ready after db synced') + } +}) // log uncaught exception process.on('uncaughtException', function (err) { - logger.error('An uncaught exception has occured.'); - logger.error(err); - logger.error('Process will exit now.'); - process.exit(1); -}); + logger.error('An uncaught exception has occured.') + logger.error(err) + logger.error('Process will exit now.') + process.exit(1) +}) // install exit handler -function handleTermSignals() { - config.maintenance = true; - // disconnect all socket.io clients - Object.keys(io.sockets.sockets).forEach(function (key) { - var socket = io.sockets.sockets[key]; - // notify client server going into maintenance status - socket.emit('maintenance'); - setTimeout(function () { - socket.disconnect(true); - }, 0); - }); - var checkCleanTimer = setInterval(function () { - if (realtime.isReady()) { - models.Revision.checkAllNotesRevision(function (err, notes) { - if (err) return logger.error(err); - if (!notes || notes.length <= 0) { - clearInterval(checkCleanTimer); - return process.exit(0); - } - }); +function handleTermSignals () { + config.maintenance = true + // disconnect all socket.io clients + Object.keys(io.sockets.sockets).forEach(function (key) { + var socket = io.sockets.sockets[key] + // notify client server going into maintenance status + socket.emit('maintenance') + setTimeout(function () { + socket.disconnect(true) + }, 0) + }) + var checkCleanTimer = setInterval(function () { + if (realtime.isReady()) { + models.Revision.checkAllNotesRevision(function (err, notes) { + if (err) return logger.error(err) + if (!notes || notes.length <= 0) { + clearInterval(checkCleanTimer) + return process.exit(0) } - }, 100); + }) + } + }, 100) } -process.on('SIGINT', handleTermSignals); -process.on('SIGTERM', handleTermSignals); +process.on('SIGINT', handleTermSignals) +process.on('SIGTERM', handleTermSignals) diff --git a/lib/auth.js b/lib/auth.js index 4b14e42c..ef1d6464 100644 --- a/lib/auth.js +++ b/lib/auth.js @@ -1,190 +1,192 @@ -//auth -//external modules -var passport = require('passport'); -var FacebookStrategy = require('passport-facebook').Strategy; -var TwitterStrategy = require('passport-twitter').Strategy; -var GithubStrategy = require('passport-github').Strategy; -var GitlabStrategy = require('passport-gitlab2').Strategy; -var DropboxStrategy = require('passport-dropbox-oauth2').Strategy; -var GoogleStrategy = require('passport-google-oauth20').Strategy; -var LdapStrategy = require('passport-ldapauth'); -var LocalStrategy = require('passport-local').Strategy; -var validator = require('validator'); +// auth +// external modules +var passport = require('passport') +var FacebookStrategy = require('passport-facebook').Strategy +var TwitterStrategy = require('passport-twitter').Strategy +var GithubStrategy = require('passport-github').Strategy +var GitlabStrategy = require('passport-gitlab2').Strategy +var DropboxStrategy = require('passport-dropbox-oauth2').Strategy +var GoogleStrategy = require('passport-google-oauth20').Strategy +var LdapStrategy = require('passport-ldapauth') +var LocalStrategy = require('passport-local').Strategy +var validator = require('validator') -//core -var config = require('./config.js'); -var logger = require("./logger.js"); -var models = require("./models"); +// core +var config = require('./config.js') +var logger = require('./logger.js') +var models = require('./models') -function callback(accessToken, refreshToken, profile, done) { - //logger.info(profile.displayName || profile.username); - var stringifiedProfile = JSON.stringify(profile); - models.User.findOrCreate({ - where: { - profileid: profile.id.toString() - }, - defaults: { - profile: stringifiedProfile, - accessToken: accessToken, - refreshToken: refreshToken - } - }).spread(function (user, created) { - if (user) { - var needSave = false; - if (user.profile != stringifiedProfile) { - user.profile = stringifiedProfile; - needSave = true; - } - if (user.accessToken != accessToken) { - user.accessToken = accessToken; - needSave = true; - } - if (user.refreshToken != refreshToken) { - user.refreshToken = refreshToken; - needSave = true; - } - if (needSave) { - user.save().then(function () { - if (config.debug) - logger.info('user login: ' + user.id); - return done(null, user); - }); - } else { - if (config.debug) - logger.info('user login: ' + user.id); - return done(null, user); - } - } - }).catch(function (err) { - logger.error('auth callback failed: ' + err); - return done(err, null); - }); +function callback (accessToken, refreshToken, profile, done) { + // logger.info(profile.displayName || profile.username); + var stringifiedProfile = JSON.stringify(profile) + models.User.findOrCreate({ + where: { + profileid: profile.id.toString() + }, + defaults: { + profile: stringifiedProfile, + accessToken: accessToken, + refreshToken: refreshToken + } + }).spread(function (user, created) { + if (user) { + var needSave = false + if (user.profile !== stringifiedProfile) { + user.profile = stringifiedProfile + needSave = true + } + if (user.accessToken !== accessToken) { + user.accessToken = accessToken + needSave = true + } + if (user.refreshToken !== refreshToken) { + user.refreshToken = refreshToken + needSave = true + } + if (needSave) { + user.save().then(function () { + if (config.debug) { logger.info('user login: ' + user.id) } + return done(null, user) + }) + } else { + if (config.debug) { logger.info('user login: ' + user.id) } + return done(null, user) + } + } + }).catch(function (err) { + logger.error('auth callback failed: ' + err) + return done(err, null) + }) } -//facebook -if (config.facebook) { - module.exports = passport.use(new FacebookStrategy({ - clientID: config.facebook.clientID, - clientSecret: config.facebook.clientSecret, - callbackURL: config.serverurl + '/auth/facebook/callback' - }, callback)); -} -//twitter -if (config.twitter) { +function registerAuthMethod () { +// facebook + if (config.facebook) { + passport.use(new FacebookStrategy({ + clientID: config.facebook.clientID, + clientSecret: config.facebook.clientSecret, + callbackURL: config.serverurl + '/auth/facebook/callback' + }, callback)) + } +// twitter + if (config.twitter) { passport.use(new TwitterStrategy({ - consumerKey: config.twitter.consumerKey, - consumerSecret: config.twitter.consumerSecret, - callbackURL: config.serverurl + '/auth/twitter/callback' - }, callback)); -} -//github -if (config.github) { + consumerKey: config.twitter.consumerKey, + consumerSecret: config.twitter.consumerSecret, + callbackURL: config.serverurl + '/auth/twitter/callback' + }, callback)) + } +// github + if (config.github) { passport.use(new GithubStrategy({ - clientID: config.github.clientID, - clientSecret: config.github.clientSecret, - callbackURL: config.serverurl + '/auth/github/callback' - }, callback)); -} -//gitlab -if (config.gitlab) { + clientID: config.github.clientID, + clientSecret: config.github.clientSecret, + callbackURL: config.serverurl + '/auth/github/callback' + }, callback)) + } +// gitlab + if (config.gitlab) { passport.use(new GitlabStrategy({ - baseURL: config.gitlab.baseURL, - clientID: config.gitlab.clientID, - clientSecret: config.gitlab.clientSecret, - callbackURL: config.serverurl + '/auth/gitlab/callback' - }, callback)); -} -//dropbox -if (config.dropbox) { + baseURL: config.gitlab.baseURL, + clientID: config.gitlab.clientID, + clientSecret: config.gitlab.clientSecret, + callbackURL: config.serverurl + '/auth/gitlab/callback' + }, callback)) + } +// dropbox + if (config.dropbox) { passport.use(new DropboxStrategy({ - apiVersion: '2', - clientID: config.dropbox.clientID, - clientSecret: config.dropbox.clientSecret, - callbackURL: config.serverurl + '/auth/dropbox/callback' - }, callback)); -} -//google -if (config.google) { + apiVersion: '2', + clientID: config.dropbox.clientID, + clientSecret: config.dropbox.clientSecret, + callbackURL: config.serverurl + '/auth/dropbox/callback' + }, callback)) + } +// google + if (config.google) { passport.use(new GoogleStrategy({ - clientID: config.google.clientID, - clientSecret: config.google.clientSecret, - callbackURL: config.serverurl + '/auth/google/callback' - }, callback)); -} + clientID: config.google.clientID, + clientSecret: config.google.clientSecret, + callbackURL: config.serverurl + '/auth/google/callback' + }, callback)) + } // ldap -if (config.ldap) { + if (config.ldap) { passport.use(new LdapStrategy({ - server: { - url: config.ldap.url || null, - bindDn: config.ldap.bindDn || null, - bindCredentials: config.ldap.bindCredentials || null, - searchBase: config.ldap.searchBase || null, - searchFilter: config.ldap.searchFilter || null, - searchAttributes: config.ldap.searchAttributes || null, - tlsOptions: config.ldap.tlsOptions || null - }, + server: { + url: config.ldap.url || null, + bindDn: config.ldap.bindDn || null, + bindCredentials: config.ldap.bindCredentials || null, + searchBase: config.ldap.searchBase || null, + searchFilter: config.ldap.searchFilter || null, + searchAttributes: config.ldap.searchAttributes || null, + tlsOptions: config.ldap.tlsOptions || null + } }, - function(user, done) { - var profile = { - id: 'LDAP-' + user.uidNumber, - username: user.uid, - displayName: user.displayName, - emails: user.mail ? [user.mail] : [], - avatarUrl: null, - profileUrl: null, - provider: 'ldap', + function (user, done) { + var profile = { + id: 'LDAP-' + user.uidNumber, + username: user.uid, + displayName: user.displayName, + emails: user.mail ? [user.mail] : [], + avatarUrl: null, + profileUrl: null, + provider: 'ldap' + } + var stringifiedProfile = JSON.stringify(profile) + models.User.findOrCreate({ + where: { + profileid: profile.id.toString() + }, + defaults: { + profile: stringifiedProfile } - var stringifiedProfile = JSON.stringify(profile); - models.User.findOrCreate({ - where: { - profileid: profile.id.toString() - }, - defaults: { - profile: stringifiedProfile, - } - }).spread(function (user, created) { - if (user) { - var needSave = false; - if (user.profile != stringifiedProfile) { - user.profile = stringifiedProfile; - needSave = true; - } - if (needSave) { - user.save().then(function () { - if (config.debug) - logger.info('user login: ' + user.id); - return done(null, user); - }); - } else { - if (config.debug) - logger.info('user login: ' + user.id); - return done(null, user); - } - } - }).catch(function (err) { - logger.error('ldap auth failed: ' + err); - return done(err, null); - }); - })); -} + }).spread(function (user, created) { + if (user) { + var needSave = false + if (user.profile !== stringifiedProfile) { + user.profile = stringifiedProfile + needSave = true + } + if (needSave) { + user.save().then(function () { + if (config.debug) { logger.info('user login: ' + user.id) } + return done(null, user) + }) + } else { + if (config.debug) { logger.info('user login: ' + user.id) } + return done(null, user) + } + } + }).catch(function (err) { + logger.error('ldap auth failed: ' + err) + return done(err, null) + }) + })) + } // email -if (config.email) { + if (config.email) { passport.use(new LocalStrategy({ - usernameField: 'email' + usernameField: 'email' }, - function(email, password, done) { - if (!validator.isEmail(email)) return done(null, false); - models.User.findOne({ - where: { - email: email - } - }).then(function (user) { - if (!user) return done(null, false); - if (!user.verifyPassword(password)) return done(null, false); - return done(null, user); - }).catch(function (err) { - logger.error(err); - return done(err); - }); - })); + function (email, password, done) { + if (!validator.isEmail(email)) return done(null, false) + models.User.findOne({ + where: { + email: email + } + }).then(function (user) { + if (!user) return done(null, false) + if (!user.verifyPassword(password)) return done(null, false) + return done(null, user) + }).catch(function (err) { + logger.error(err) + return done(err) + }) + })) + } +} + +module.exports = { + registerAuthMethod: registerAuthMethod } diff --git a/lib/config.js b/lib/config.js index 4d2fbf74..af4c22cd 100644 --- a/lib/config.js +++ b/lib/config.js @@ -1,118 +1,117 @@ // external modules -var fs = require('fs'); -var path = require('path'); -var fs = require('fs'); +var fs = require('fs') +var path = require('path') // configs -var env = process.env.NODE_ENV || 'development'; -var config = require(path.join(__dirname, '..', 'config.json'))[env]; -var debug = process.env.DEBUG ? (process.env.DEBUG === 'true') : ((typeof config.debug === 'boolean') ? config.debug : (env === 'development')); +var env = process.env.NODE_ENV || 'development' +var config = require(path.join(__dirname, '..', 'config.json'))[env] +var debug = process.env.DEBUG ? (process.env.DEBUG === 'true') : ((typeof config.debug === 'boolean') ? config.debug : (env === 'development')) // Create function that reads docker secrets but fails fast in case of a non docker environment -var handleDockerSecret = fs.existsSync('/run/secrets/') ? function(secret) { - return fs.existsSync('/run/secrets/' + secret) ? fs.readFileSync('/run/secrets/' + secret) : null; -} : function() { - return null -}; +var handleDockerSecret = fs.existsSync('/run/secrets/') ? function (secret) { + return fs.existsSync('/run/secrets/' + secret) ? fs.readFileSync('/run/secrets/' + secret) : null +} : function () { + return null +} // url -var domain = process.env.DOMAIN || process.env.HMD_DOMAIN || config.domain || ''; -var urlpath = process.env.URL_PATH || process.env.HMD_URL_PATH || config.urlpath || ''; -var port = process.env.PORT || process.env.HMD_PORT || config.port || 3000; -var alloworigin = process.env.HMD_ALLOW_ORIGIN ? process.env.HMD_ALLOW_ORIGIN.split(',') : (config.alloworigin || ['localhost']); +var domain = process.env.DOMAIN || process.env.HMD_DOMAIN || config.domain || '' +var urlpath = process.env.URL_PATH || process.env.HMD_URL_PATH || config.urlpath || '' +var port = process.env.PORT || process.env.HMD_PORT || config.port || 3000 +var alloworigin = process.env.HMD_ALLOW_ORIGIN ? process.env.HMD_ALLOW_ORIGIN.split(',') : (config.alloworigin || ['localhost']) -var usessl = !!config.usessl; +var usessl = !!config.usessl var protocolusessl = (usessl === true && typeof process.env.HMD_PROTOCOL_USESSL === 'undefined' && typeof config.protocolusessl === 'undefined') - ? true : (process.env.HMD_PROTOCOL_USESSL ? (process.env.HMD_PROTOCOL_USESSL === 'true') : !!config.protocolusessl); -var urladdport = process.env.HMD_URL_ADDPORT ? (process.env.HMD_URL_ADDPORT === 'true') : !!config.urladdport; + ? true : (process.env.HMD_PROTOCOL_USESSL ? (process.env.HMD_PROTOCOL_USESSL === 'true') : !!config.protocolusessl) +var urladdport = process.env.HMD_URL_ADDPORT ? (process.env.HMD_URL_ADDPORT === 'true') : !!config.urladdport -var usecdn = process.env.HMD_USECDN ? (process.env.HMD_USECDN === 'true') : ((typeof config.usecdn === 'boolean') ? config.usecdn : true); +var usecdn = process.env.HMD_USECDN ? (process.env.HMD_USECDN === 'true') : ((typeof config.usecdn === 'boolean') ? config.usecdn : true) -var allowanonymous = process.env.HMD_ALLOW_ANONYMOUS ? (process.env.HMD_ALLOW_ANONYMOUS === 'true') : ((typeof config.allowanonymous === 'boolean') ? config.allowanonymous : true); +var allowanonymous = process.env.HMD_ALLOW_ANONYMOUS ? (process.env.HMD_ALLOW_ANONYMOUS === 'true') : ((typeof config.allowanonymous === 'boolean') ? config.allowanonymous : true) -var allowfreeurl = process.env.HMD_ALLOW_FREEURL ? (process.env.HMD_ALLOW_FREEURL === 'true') : !!config.allowfreeurl; +var allowfreeurl = process.env.HMD_ALLOW_FREEURL ? (process.env.HMD_ALLOW_FREEURL === 'true') : !!config.allowfreeurl -var permissions = ['editable', 'limited', 'locked', 'protected', 'private']; +var permissions = ['editable', 'limited', 'locked', 'protected', 'private'] if (allowanonymous) { - permissions.unshift('freely'); + permissions.unshift('freely') } -var defaultpermission = process.env.HMD_DEFAULT_PERMISSION || config.defaultpermission; -defaultpermission = permissions.indexOf(defaultpermission) != -1 ? defaultpermission : 'editable'; +var defaultpermission = process.env.HMD_DEFAULT_PERMISSION || config.defaultpermission +defaultpermission = permissions.indexOf(defaultpermission) !== -1 ? defaultpermission : 'editable' // db -var dburl = process.env.HMD_DB_URL || process.env.DATABASE_URL || config.dburl; -var db = config.db || {}; +var dburl = process.env.HMD_DB_URL || process.env.DATABASE_URL || config.dburl +var db = config.db || {} // ssl path -var sslkeypath = (fs.existsSync('/run/secrets/key.pem') ? '/run/secrets/key.pem' : null) || config.sslkeypath || ''; -var sslcertpath = (fs.existsSync('/run/secrets/cert.pem') ? '/run/secrets/cert.pem' : null) || config.sslcertpath || ''; -var sslcapath = (fs.existsSync('/run/secrets/ca.pem') ? '/run/secrets/ca.pem' : null) || config.sslcapath || ''; -var dhparampath = (fs.existsSync('/run/secrets/dhparam.pem') ? '/run/secrets/dhparam.pem' : null) || config.dhparampath || ''; +var sslkeypath = (fs.existsSync('/run/secrets/key.pem') ? '/run/secrets/key.pem' : null) || config.sslkeypath || '' +var sslcertpath = (fs.existsSync('/run/secrets/cert.pem') ? '/run/secrets/cert.pem' : null) || config.sslcertpath || '' +var sslcapath = (fs.existsSync('/run/secrets/ca.pem') ? '/run/secrets/ca.pem' : null) || config.sslcapath || '' +var dhparampath = (fs.existsSync('/run/secrets/dhparam.pem') ? '/run/secrets/dhparam.pem' : null) || config.dhparampath || '' // other path -var tmppath = config.tmppath || './tmp'; -var defaultnotepath = config.defaultnotepath || './public/default.md'; -var docspath = config.docspath || './public/docs'; -var indexpath = config.indexpath || './public/views/index.ejs'; -var hackmdpath = config.hackmdpath || './public/views/hackmd.ejs'; -var errorpath = config.errorpath || './public/views/error.ejs'; -var prettypath = config.prettypath || './public/views/pretty.ejs'; -var slidepath = config.slidepath || './public/views/slide.ejs'; +var tmppath = config.tmppath || './tmp' +var defaultnotepath = config.defaultnotepath || './public/default.md' +var docspath = config.docspath || './public/docs' +var indexpath = config.indexpath || './public/views/index.ejs' +var hackmdpath = config.hackmdpath || './public/views/hackmd.ejs' +var errorpath = config.errorpath || './public/views/error.ejs' +var prettypath = config.prettypath || './public/views/pretty.ejs' +var slidepath = config.slidepath || './public/views/slide.ejs' // session -var sessionname = config.sessionname || 'connect.sid'; -var sessionsecret = handleDockerSecret('sessionsecret') || config.sessionsecret || 'secret'; -var sessionlife = config.sessionlife || 14 * 24 * 60 * 60 * 1000; //14 days +var sessionname = config.sessionname || 'connect.sid' +var sessionsecret = handleDockerSecret('sessionsecret') || config.sessionsecret || 'secret' +var sessionlife = config.sessionlife || 14 * 24 * 60 * 60 * 1000 // 14 days // static files -var staticcachetime = config.staticcachetime || 1 * 24 * 60 * 60 * 1000; // 1 day +var staticcachetime = config.staticcachetime || 1 * 24 * 60 * 60 * 1000 // 1 day // socket.io -var heartbeatinterval = config.heartbeatinterval || 5000; -var heartbeattimeout = config.heartbeattimeout || 10000; +var heartbeatinterval = config.heartbeatinterval || 5000 +var heartbeattimeout = config.heartbeattimeout || 10000 // document -var documentmaxlength = config.documentmaxlength || 100000; +var documentmaxlength = config.documentmaxlength || 100000 // image upload setting, available options are imgur/s3/filesystem -var imageUploadType = process.env.HMD_IMAGE_UPLOAD_TYPE || config.imageUploadType || 'imgur'; +var imageUploadType = process.env.HMD_IMAGE_UPLOAD_TYPE || config.imageUploadType || 'imgur' -config.s3 = config.s3 || {}; +config.s3 = config.s3 || {} var s3 = { - accessKeyId: handleDockerSecret('s3_acccessKeyId') || process.env.HMD_S3_ACCESS_KEY_ID || config.s3.accessKeyId, - secretAccessKey: handleDockerSecret('s3_secretAccessKey') || process.env.HMD_S3_SECRET_ACCESS_KEY || config.s3.secretAccessKey, - region: process.env.HMD_S3_REGION || config.s3.region + accessKeyId: handleDockerSecret('s3_acccessKeyId') || process.env.HMD_S3_ACCESS_KEY_ID || config.s3.accessKeyId, + secretAccessKey: handleDockerSecret('s3_secretAccessKey') || process.env.HMD_S3_SECRET_ACCESS_KEY || config.s3.secretAccessKey, + region: process.env.HMD_S3_REGION || config.s3.region } -var s3bucket = process.env.HMD_S3_BUCKET || config.s3.bucket; +var s3bucket = process.env.HMD_S3_BUCKET || config.s3.bucket // auth -var facebook = (process.env.HMD_FACEBOOK_CLIENTID && process.env.HMD_FACEBOOK_CLIENTSECRET || fs.existsSync('/run/secrets/facebook_clientID') && fs.existsSync('/run/secrets/facebook_clientSecret')) ? { - clientID: handleDockerSecret('facebook_clientID') || process.env.HMD_FACEBOOK_CLIENTID, - clientSecret: handleDockerSecret('facebook_clientSecret') || process.env.HMD_FACEBOOK_CLIENTSECRET -} : config.facebook || false; -var twitter = (process.env.HMD_TWITTER_CONSUMERKEY && process.env.HMD_TWITTER_CONSUMERSECRET || fs.existsSync('/run/secrets/twitter_consumerKey') && fs.existsSync('/run/secrets/twitter_consumerSecret')) ? { - consumerKey: handleDockerSecret('twitter_consumerKey') || process.env.HMD_TWITTER_CONSUMERKEY, - consumerSecret: handleDockerSecret('twitter_consumerSecret') || process.env.HMD_TWITTER_CONSUMERSECRET -} : config.twitter || false; -var github = (process.env.HMD_GITHUB_CLIENTID && process.env.HMD_GITHUB_CLIENTSECRET || fs.existsSync('/run/secrets/github_clientID') && fs.existsSync('/run/secrets/github_clientSecret')) ? { - clientID: handleDockerSecret('github_clientID') || process.env.HMD_GITHUB_CLIENTID, - clientSecret: handleDockerSecret('github_clientSecret') || process.env.HMD_GITHUB_CLIENTSECRET -} : config.github || false; -var gitlab = (process.env.HMD_GITLAB_CLIENTID && process.env.HMD_GITLAB_CLIENTSECRET || fs.existsSync('/run/secrets/gitlab_clientID') && fs.existsSync('/run/secrets/gitlab_clientSecret')) ? { - baseURL: process.env.HMD_GITLAB_BASEURL, - clientID: handleDockerSecret('gitlab_clientID') || process.env.HMD_GITLAB_CLIENTID, - clientSecret: handleDockerSecret('gitlab_clientSecret') || process.env.HMD_GITLAB_CLIENTSECRET -} : config.gitlab || false; +var facebook = ((process.env.HMD_FACEBOOK_CLIENTID && process.env.HMD_FACEBOOK_CLIENTSECRET) || (fs.existsSync('/run/secrets/facebook_clientID') && fs.existsSync('/run/secrets/facebook_clientSecret'))) ? { + clientID: handleDockerSecret('facebook_clientID') || process.env.HMD_FACEBOOK_CLIENTID, + clientSecret: handleDockerSecret('facebook_clientSecret') || process.env.HMD_FACEBOOK_CLIENTSECRET +} : config.facebook || false +var twitter = ((process.env.HMD_TWITTER_CONSUMERKEY && process.env.HMD_TWITTER_CONSUMERSECRET) || (fs.existsSync('/run/secrets/twitter_consumerKey') && fs.existsSync('/run/secrets/twitter_consumerSecret'))) ? { + consumerKey: handleDockerSecret('twitter_consumerKey') || process.env.HMD_TWITTER_CONSUMERKEY, + consumerSecret: handleDockerSecret('twitter_consumerSecret') || process.env.HMD_TWITTER_CONSUMERSECRET +} : config.twitter || false +var github = ((process.env.HMD_GITHUB_CLIENTID && process.env.HMD_GITHUB_CLIENTSECRET) || (fs.existsSync('/run/secrets/github_clientID') && fs.existsSync('/run/secrets/github_clientSecret'))) ? { + clientID: handleDockerSecret('github_clientID') || process.env.HMD_GITHUB_CLIENTID, + clientSecret: handleDockerSecret('github_clientSecret') || process.env.HMD_GITHUB_CLIENTSECRET +} : config.github || false +var gitlab = ((process.env.HMD_GITLAB_CLIENTID && process.env.HMD_GITLAB_CLIENTSECRET) || (fs.existsSync('/run/secrets/gitlab_clientID') && fs.existsSync('/run/secrets/gitlab_clientSecret'))) ? { + baseURL: process.env.HMD_GITLAB_BASEURL, + 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'))) ? { - clientID: handleDockerSecret('dropbox_clientID') || process.env.HMD_DROPBOX_CLIENTID, - clientSecret: handleDockerSecret('dropbox_clientSecret') || process.env.HMD_DROPBOX_CLIENTSECRET -} : (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; + clientID: handleDockerSecret('dropbox_clientID') || process.env.HMD_DROPBOX_CLIENTID, + clientSecret: handleDockerSecret('dropbox_clientSecret') || process.env.HMD_DROPBOX_CLIENTSECRET +} : (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 || @@ -123,106 +122,97 @@ var ldap = config.ldap || (( process.env.HMD_LDAP_SEARCHATTRIBUTES || process.env.HMD_LDAP_TLS_CA || process.env.HMD_LDAP_PROVIDERNAME -) ? {} : false); -if (process.env.HMD_LDAP_URL) - ldap.url = process.env.HMD_LDAP_URL; -if (process.env.HMD_LDAP_BINDDN) - ldap.bindDn = process.env.HMD_LDAP_BINDDN; -if (process.env.HMD_LDAP_BINDCREDENTIALS) - ldap.bindCredentials = process.env.HMD_LDAP_BINDCREDENTIALS; -if (process.env.HMD_LDAP_TOKENSECRET) - ldap.tokenSecret = process.env.HMD_LDAP_TOKENSECRET; -if (process.env.HMD_LDAP_SEARCHBASE) - ldap.searchBase = process.env.HMD_LDAP_SEARCHBASE; -if (process.env.HMD_LDAP_SEARCHFILTER) - ldap.searchFilter = process.env.HMD_LDAP_SEARCHFILTER; -if (process.env.HMD_LDAP_SEARCHATTRIBUTES) - ldap.searchAttributes = process.env.HMD_LDAP_SEARCHATTRIBUTES; +) ? {} : false) +if (process.env.HMD_LDAP_URL) { ldap.url = process.env.HMD_LDAP_URL } +if (process.env.HMD_LDAP_BINDDN) { ldap.bindDn = process.env.HMD_LDAP_BINDDN } +if (process.env.HMD_LDAP_BINDCREDENTIALS) { ldap.bindCredentials = process.env.HMD_LDAP_BINDCREDENTIALS } +if (process.env.HMD_LDAP_TOKENSECRET) { ldap.tokenSecret = process.env.HMD_LDAP_TOKENSECRET } +if (process.env.HMD_LDAP_SEARCHBASE) { ldap.searchBase = process.env.HMD_LDAP_SEARCHBASE } +if (process.env.HMD_LDAP_SEARCHFILTER) { ldap.searchFilter = process.env.HMD_LDAP_SEARCHFILTER } +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.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; + var 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 + } } if (process.env.HMD_LDAP_PROVIDERNAME) { - ldap.providerName = process.env.HMD_LDAP_PROVIDERNAME; + ldap.providerName = process.env.HMD_LDAP_PROVIDERNAME } -var imgur = handleDockerSecret('imgur_clientid') || process.env.HMD_IMGUR_CLIENTID || config.imgur || false; -var email = process.env.HMD_EMAIL ? (process.env.HMD_EMAIL === 'true') : !!config.email; -var allowemailregister = process.env.HMD_ALLOW_EMAIL_REGISTER ? (process.env.HMD_ALLOW_EMAIL_REGISTER === 'true') : ((typeof config.allowemailregister === 'boolean') ? config.allowemailregister : true); - -function getserverurl() { - var url = ''; - if (domain) { - var protocol = protocolusessl ? 'https://' : 'http://'; - url = protocol + domain; - if (urladdport && ((usessl && port != 443) || (!usessl && port != 80))) - url += ':' + port; - } - if (urlpath) - url += '/' + urlpath; - return url; +var imgur = handleDockerSecret('imgur_clientid') || process.env.HMD_IMGUR_CLIENTID || config.imgur || false +var email = process.env.HMD_EMAIL ? (process.env.HMD_EMAIL === 'true') : !!config.email +var allowemailregister = process.env.HMD_ALLOW_EMAIL_REGISTER ? (process.env.HMD_ALLOW_EMAIL_REGISTER === 'true') : ((typeof config.allowemailregister === 'boolean') ? config.allowemailregister : true) + +function getserverurl () { + var url = '' + if (domain) { + var protocol = protocolusessl ? 'https://' : 'http://' + url = protocol + domain + if (urladdport && ((usessl && port !== 443) || (!usessl && port !== 80))) { url += ':' + port } + } + if (urlpath) { url += '/' + urlpath } + return url } -var version = '0.5.0'; -var minimumCompatibleVersion = '0.5.0'; -var maintenance = true; -var cwd = path.join(__dirname, '..'); +var version = '0.5.0' +var minimumCompatibleVersion = '0.5.0' +var maintenance = true +var cwd = path.join(__dirname, '..') module.exports = { - version: version, - minimumCompatibleVersion: minimumCompatibleVersion, - maintenance: maintenance, - debug: debug, - urlpath: urlpath, - port: port, - alloworigin: alloworigin, - usessl: usessl, - serverurl: getserverurl(), - usecdn: usecdn, - allowanonymous: allowanonymous, - allowfreeurl: allowfreeurl, - defaultpermission: defaultpermission, - dburl: dburl, - db: db, - sslkeypath: path.join(cwd, sslkeypath), - sslcertpath: path.join(cwd, sslcertpath), - sslcapath: path.join(cwd, sslcapath), - dhparampath: path.join(cwd, dhparampath), - tmppath: path.join(cwd, tmppath), - defaultnotepath: path.join(cwd, defaultnotepath), - docspath: path.join(cwd, docspath), - indexpath: path.join(cwd, indexpath), - hackmdpath: path.join(cwd, hackmdpath), - errorpath: path.join(cwd, errorpath), - prettypath: path.join(cwd, prettypath), - slidepath: path.join(cwd, slidepath), - sessionname: sessionname, - sessionsecret: sessionsecret, - sessionlife: sessionlife, - staticcachetime: staticcachetime, - heartbeatinterval: heartbeatinterval, - heartbeattimeout: heartbeattimeout, - documentmaxlength: documentmaxlength, - facebook: facebook, - twitter: twitter, - github: github, - gitlab: gitlab, - dropbox: dropbox, - google: google, - ldap: ldap, - imgur: imgur, - email: email, - allowemailregister: allowemailregister, - imageUploadType: imageUploadType, - s3: s3, - s3bucket: s3bucket -}; + version: version, + minimumCompatibleVersion: minimumCompatibleVersion, + maintenance: maintenance, + debug: debug, + urlpath: urlpath, + port: port, + alloworigin: alloworigin, + usessl: usessl, + serverurl: getserverurl(), + usecdn: usecdn, + allowanonymous: allowanonymous, + allowfreeurl: allowfreeurl, + defaultpermission: defaultpermission, + dburl: dburl, + db: db, + sslkeypath: path.join(cwd, sslkeypath), + sslcertpath: path.join(cwd, sslcertpath), + sslcapath: path.join(cwd, sslcapath), + dhparampath: path.join(cwd, dhparampath), + tmppath: path.join(cwd, tmppath), + defaultnotepath: path.join(cwd, defaultnotepath), + docspath: path.join(cwd, docspath), + indexpath: path.join(cwd, indexpath), + hackmdpath: path.join(cwd, hackmdpath), + errorpath: path.join(cwd, errorpath), + prettypath: path.join(cwd, prettypath), + slidepath: path.join(cwd, slidepath), + sessionname: sessionname, + sessionsecret: sessionsecret, + sessionlife: sessionlife, + staticcachetime: staticcachetime, + heartbeatinterval: heartbeatinterval, + heartbeattimeout: heartbeattimeout, + documentmaxlength: documentmaxlength, + facebook: facebook, + twitter: twitter, + github: github, + gitlab: gitlab, + dropbox: dropbox, + google: google, + ldap: ldap, + imgur: imgur, + email: email, + allowemailregister: allowemailregister, + imageUploadType: imageUploadType, + s3: s3, + s3bucket: s3bucket +} diff --git a/lib/history.js b/lib/history.js index e7fb3087..69337dc5 100644 --- a/lib/history.js +++ b/lib/history.js @@ -1,172 +1,175 @@ -//history -//external modules -var async = require('async'); +// history +// external modules -//core -var config = require("./config.js"); -var logger = require("./logger.js"); -var response = require("./response.js"); -var models = require("./models"); +// core +var config = require('./config.js') +var logger = require('./logger.js') +var response = require('./response.js') +var models = require('./models') -//public +// public var History = { - historyGet: historyGet, - historyPost: historyPost, - historyDelete: historyDelete, - updateHistory: updateHistory -}; - -function getHistory(userid, callback) { - models.User.findOne({ - where: { - id: userid - } - }).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); - }); + historyGet: historyGet, + historyPost: historyPost, + historyDelete: historyDelete, + updateHistory: updateHistory } -function setHistory(userid, history, callback) { - models.User.update({ - history: JSON.stringify(parseHistoryToArray(history)) - }, { - where: { - id: userid - } - }).then(function (count) { - return callback(null, count); - }).catch(function (err) { - logger.error('set history failed: ' + err); - return callback(err, null); - }); +function getHistory (userid, callback) { + models.User.findOne({ + where: { + id: userid + } + }).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) + }) } -function updateHistory(userid, noteId, document, time) { - if (userid && noteId && typeof document !== 'undefined') { - getHistory(userid, function (err, history) { - if (err || !history) return; - if (!history[noteId]) { - history[noteId] = {}; - } - var noteHistory = history[noteId]; - var noteInfo = models.Note.parseNoteInfo(document); - noteHistory.id = noteId; - noteHistory.text = noteInfo.title; - noteHistory.time = time || Date.now(); - noteHistory.tags = noteInfo.tags; - setHistory(userid, history, function (err, count) { - return; - }); - }); +function setHistory (userid, history, callback) { + models.User.update({ + history: JSON.stringify(parseHistoryToArray(history)) + }, { + where: { + id: userid } + }).then(function (count) { + return callback(null, count) + }).catch(function (err) { + logger.error('set history failed: ' + err) + return callback(err, null) + }) } -function parseHistoryToArray(history) { - var _history = []; - Object.keys(history).forEach(function (key) { - var item = history[key]; - _history.push(item); - }); - return _history; +function updateHistory (userid, noteId, document, time) { + if (userid && noteId && typeof document !== 'undefined') { + getHistory(userid, function (err, history) { + if (err || !history) return + if (!history[noteId]) { + history[noteId] = {} + } + var noteHistory = history[noteId] + var noteInfo = models.Note.parseNoteInfo(document) + noteHistory.id = noteId + noteHistory.text = noteInfo.title + noteHistory.time = time || Date.now() + noteHistory.tags = noteInfo.tags + setHistory(userid, history, function (err, count) { + if (err) { + logger.log(err) + } + }) + }) + } } -function parseHistoryToObject(history) { - var _history = {}; - for (var i = 0, l = history.length; i < l; i++) { - var item = history[i]; - _history[item.id] = item; - } - return _history; +function parseHistoryToArray (history) { + var _history = [] + Object.keys(history).forEach(function (key) { + var item = history[key] + _history.push(item) + }) + return _history } -function historyGet(req, res) { - if (req.isAuthenticated()) { - getHistory(req.user.id, function (err, history) { - if (err) return response.errorInternalError(res); - if (!history) return response.errorNotFound(res); - res.send({ - history: parseHistoryToArray(history) - }); - }); - } else { - return response.errorForbidden(res); - } +function parseHistoryToObject (history) { + var _history = {} + for (var i = 0, l = history.length; i < l; i++) { + var item = history[i] + _history[item.id] = item + } + return _history +} + +function historyGet (req, res) { + if (req.isAuthenticated()) { + getHistory(req.user.id, function (err, history) { + if (err) return response.errorInternalError(res) + if (!history) return response.errorNotFound(res) + res.send({ + history: parseHistoryToArray(history) + }) + }) + } else { + return response.errorForbidden(res) + } } -function historyPost(req, res) { - if (req.isAuthenticated()) { - var noteId = req.params.noteId; - if (!noteId) { - if (typeof req.body['history'] === 'undefined') return response.errorBadRequest(res); - if (config.debug) - logger.info('SERVER received history from [' + req.user.id + ']: ' + req.body.history); - try { - var history = JSON.parse(req.body.history); - } catch (err) { - return response.errorBadRequest(res); - } - if (Array.isArray(history)) { - setHistory(req.user.id, history, function (err, count) { - if (err) return response.errorInternalError(res); - res.end(); - }); - } else { - return response.errorBadRequest(res); - } +function historyPost (req, res) { + if (req.isAuthenticated()) { + var noteId = req.params.noteId + if (!noteId) { + if (typeof req.body['history'] === 'undefined') return response.errorBadRequest(res) + if (config.debug) { logger.info('SERVER received history from [' + req.user.id + ']: ' + req.body.history) } + try { + var history = JSON.parse(req.body.history) + } catch (err) { + return response.errorBadRequest(res) + } + if (Array.isArray(history)) { + setHistory(req.user.id, history, function (err, count) { + if (err) return response.errorInternalError(res) + res.end() + }) + } else { + return response.errorBadRequest(res) + } + } else { + if (typeof req.body['pinned'] === 'undefined') return response.errorBadRequest(res) + getHistory(req.user.id, function (err, history) { + if (err) return response.errorInternalError(res) + if (!history) return response.errorNotFound(res) + if (!history[noteId]) return response.errorNotFound(res) + if (req.body.pinned === 'true' || req.body.pinned === 'false') { + history[noteId].pinned = (req.body.pinned === 'true') + setHistory(req.user.id, history, function (err, count) { + if (err) return response.errorInternalError(res) + res.end() + }) } else { - if (typeof req.body['pinned'] === 'undefined') return response.errorBadRequest(res); - getHistory(req.user.id, function (err, history) { - if (err) return response.errorInternalError(res); - if (!history) return response.errorNotFound(res); - if (!history[noteId]) return response.errorNotFound(res); - if (req.body.pinned === 'true' || req.body.pinned === 'false') { - 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); - } - }); + return response.errorBadRequest(res) } - } else { - return response.errorForbidden(res); + }) } + } else { + return response.errorForbidden(res) + } } -function historyDelete(req, res) { - if (req.isAuthenticated()) { - var noteId = req.params.noteId; - if (!noteId) { - 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 history[noteId]; - setHistory(req.user.id, history, function (err, count) { - if (err) return response.errorInternalError(res); - res.end(); - }); - }); - } +function historyDelete (req, res) { + if (req.isAuthenticated()) { + var noteId = req.params.noteId + if (!noteId) { + setHistory(req.user.id, [], function (err, count) { + if (err) return response.errorInternalError(res) + res.end() + }) } else { - return response.errorForbidden(res); + getHistory(req.user.id, function (err, history) { + if (err) return response.errorInternalError(res) + if (!history) return response.errorNotFound(res) + delete history[noteId] + setHistory(req.user.id, history, function (err, count) { + if (err) return response.errorInternalError(res) + res.end() + }) + }) } + } else { + return response.errorForbidden(res) + } } -module.exports = History;
\ No newline at end of file +module.exports = History diff --git a/lib/letter-avatars.js b/lib/letter-avatars.js index 3afa03fe..92bd36ee 100644 --- a/lib/letter-avatars.js +++ b/lib/letter-avatars.js @@ -1,25 +1,23 @@ -"use strict"; - // external modules -var randomcolor = require('randomcolor'); +var randomcolor = require('randomcolor') // core -module.exports = function(name) { - var color = randomcolor({ - seed: name, - luminosity: 'dark' - }); - var letter = name.substring(0, 1).toUpperCase(); +module.exports = function (name) { + var color = randomcolor({ + seed: name, + luminosity: 'dark' + }) + var letter = name.substring(0, 1).toUpperCase() - var svg = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>'; - svg += '<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="96" width="96" version="1.1" viewBox="0 0 96 96">'; - svg += '<g>'; - svg += '<rect width="96" height="96" fill="' + color + '" />'; - svg += '<text font-size="64px" font-family="sans-serif" text-anchor="middle" fill="#ffffff">'; - svg += '<tspan x="48" y="72" stroke-width=".26458px" fill="#ffffff">' + letter + '</tspan>'; - svg += '</text>'; - svg += '</g>'; - svg += '</svg>'; + var svg = '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' + svg += '<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="96" width="96" version="1.1" viewBox="0 0 96 96">' + svg += '<g>' + svg += '<rect width="96" height="96" fill="' + color + '" />' + svg += '<text font-size="64px" font-family="sans-serif" text-anchor="middle" fill="#ffffff">' + svg += '<tspan x="48" y="72" stroke-width=".26458px" fill="#ffffff">' + letter + '</tspan>' + svg += '</text>' + svg += '</g>' + svg += '</svg>' - return 'data:image/svg+xml;base64,' + new Buffer(svg).toString('base64'); -}; + return 'data:image/svg+xml;base64,' + new Buffer(svg).toString('base64') +} diff --git a/lib/logger.js b/lib/logger.js index 61299c10..23e302da 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -1,22 +1,22 @@ -var winston = require('winston'); -winston.emitErrs = true; +var winston = require('winston') +winston.emitErrs = true var logger = new winston.Logger({ - transports: [ - new winston.transports.Console({ - level: 'debug', - handleExceptions: true, - json: false, - colorize: true, - timestamp: true - }) - ], - exitOnError: false -}); + transports: [ + new winston.transports.Console({ + level: 'debug', + handleExceptions: true, + json: false, + colorize: true, + timestamp: true + }) + ], + exitOnError: false +}) -module.exports = logger; +module.exports = logger module.exports.stream = { - write: function(message, encoding){ - logger.info(message); - } -};
\ No newline at end of file + write: function (message, encoding) { + logger.info(message) + } +} diff --git a/lib/migrations/20160515114000-user-add-tokens.js b/lib/migrations/20160515114000-user-add-tokens.js index 3af490a9..20c0e03c 100644 --- a/lib/migrations/20160515114000-user-add-tokens.js +++ b/lib/migrations/20160515114000-user-add-tokens.js @@ -1,15 +1,11 @@ -"use strict"; - module.exports = { - up: function (queryInterface, Sequelize) { - queryInterface.addColumn('Users', 'accessToken', Sequelize.STRING); - queryInterface.addColumn('Users', 'refreshToken', Sequelize.STRING); - return; - }, + up: function (queryInterface, Sequelize) { + queryInterface.addColumn('Users', 'accessToken', Sequelize.STRING) + queryInterface.addColumn('Users', 'refreshToken', Sequelize.STRING) + }, - down: function (queryInterface, Sequelize) { - queryInterface.removeColumn('Users', 'accessToken'); - queryInterface.removeColumn('Users', 'refreshToken'); - return; - } -};
\ No newline at end of file + down: function (queryInterface, Sequelize) { + queryInterface.removeColumn('Users', 'accessToken') + queryInterface.removeColumn('Users', 'refreshToken') + } +} diff --git a/lib/migrations/20160607060246-support-revision.js b/lib/migrations/20160607060246-support-revision.js index fa647d93..618bb4d7 100644 --- a/lib/migrations/20160607060246-support-revision.js +++ b/lib/migrations/20160607060246-support-revision.js @@ -1,8 +1,6 @@ -'use strict'; - module.exports = { up: function (queryInterface, Sequelize) { - queryInterface.addColumn('Notes', 'savedAt', Sequelize.DATE); + queryInterface.addColumn('Notes', 'savedAt', Sequelize.DATE) queryInterface.createTable('Revisions', { id: { type: Sequelize.UUID, @@ -15,13 +13,11 @@ module.exports = { length: Sequelize.INTEGER, createdAt: Sequelize.DATE, updatedAt: Sequelize.DATE - }); - return; + }) }, down: function (queryInterface, Sequelize) { - queryInterface.dropTable('Revisions'); - queryInterface.removeColumn('Notes', 'savedAt'); - return; + queryInterface.dropTable('Revisions') + queryInterface.removeColumn('Notes', 'savedAt') } -}; +} diff --git a/lib/migrations/20160703062241-support-authorship.js b/lib/migrations/20160703062241-support-authorship.js index 239327ec..98381d4e 100644 --- a/lib/migrations/20160703062241-support-authorship.js +++ b/lib/migrations/20160703062241-support-authorship.js @@ -1,9 +1,7 @@ -'use strict'; - module.exports = { up: function (queryInterface, Sequelize) { - queryInterface.addColumn('Notes', 'authorship', Sequelize.TEXT); - queryInterface.addColumn('Revisions', 'authorship', Sequelize.TEXT); + queryInterface.addColumn('Notes', 'authorship', Sequelize.TEXT) + queryInterface.addColumn('Revisions', 'authorship', Sequelize.TEXT) queryInterface.createTable('Authors', { id: { type: Sequelize.INTEGER, @@ -15,14 +13,12 @@ module.exports = { userId: Sequelize.UUID, createdAt: Sequelize.DATE, updatedAt: Sequelize.DATE - }); - return; + }) }, down: function (queryInterface, Sequelize) { - queryInterface.dropTable('Authors'); - queryInterface.removeColumn('Revisions', 'authorship'); - queryInterface.removeColumn('Notes', 'authorship'); - return; + queryInterface.dropTable('Authors') + queryInterface.removeColumn('Revisions', 'authorship') + queryInterface.removeColumn('Notes', 'authorship') } -}; +} diff --git a/lib/migrations/20161009040430-support-delete-note.js b/lib/migrations/20161009040430-support-delete-note.js index 92ff6f7b..984920b8 100644 --- a/lib/migrations/20161009040430-support-delete-note.js +++ b/lib/migrations/20161009040430-support-delete-note.js @@ -1,11 +1,9 @@ -'use strict'; - module.exports = { up: function (queryInterface, Sequelize) { - queryInterface.addColumn('Notes', 'deletedAt', Sequelize.DATE); + queryInterface.addColumn('Notes', 'deletedAt', Sequelize.DATE) }, down: function (queryInterface, Sequelize) { - queryInterface.removeColumn('Notes', 'deletedAt'); + queryInterface.removeColumn('Notes', 'deletedAt') } -}; +} diff --git a/lib/migrations/20161201050312-support-email-signin.js b/lib/migrations/20161201050312-support-email-signin.js index b5aaf777..a97d3be5 100644 --- a/lib/migrations/20161201050312-support-email-signin.js +++ b/lib/migrations/20161201050312-support-email-signin.js @@ -1,13 +1,11 @@ -'use strict'; - module.exports = { up: function (queryInterface, Sequelize) { - queryInterface.addColumn('Users', 'email', Sequelize.TEXT); - queryInterface.addColumn('Users', 'password', Sequelize.TEXT); + queryInterface.addColumn('Users', 'email', Sequelize.TEXT) + queryInterface.addColumn('Users', 'password', Sequelize.TEXT) }, down: function (queryInterface, Sequelize) { - queryInterface.removeColumn('Users', 'email'); - queryInterface.removeColumn('Users', 'password'); + queryInterface.removeColumn('Users', 'email') + queryInterface.removeColumn('Users', 'password') } -}; +} diff --git a/lib/models/author.js b/lib/models/author.js index 0b0f149d..5e39c347 100644 --- a/lib/models/author.js +++ b/lib/models/author.js @@ -1,43 +1,37 @@ -"use strict"; - // external modules -var Sequelize = require("sequelize"); - -// core -var logger = require("../logger.js"); +var Sequelize = require('sequelize') module.exports = function (sequelize, DataTypes) { - var Author = sequelize.define("Author", { - id: { - type: Sequelize.INTEGER, - primaryKey: true, - autoIncrement: true - }, - color: { - type: DataTypes.STRING - } - }, { - indexes: [ - { - unique: true, - fields: ['noteId', 'userId'] - } - ], - classMethods: { - associate: function (models) { - Author.belongsTo(models.Note, { - foreignKey: "noteId", - as: "note", - constraints: false - }); - Author.belongsTo(models.User, { - foreignKey: "userId", - as: "user", - constraints: false - }); - } - } - }); - - return Author; -};
\ No newline at end of file + var Author = sequelize.define('Author', { + id: { + type: Sequelize.INTEGER, + primaryKey: true, + autoIncrement: true + }, + color: { + type: DataTypes.STRING + } + }, { + indexes: [ + { + unique: true, + fields: ['noteId', 'userId'] + } + ], + classMethods: { + associate: function (models) { + Author.belongsTo(models.Note, { + foreignKey: 'noteId', + as: 'note', + constraints: false + }) + Author.belongsTo(models.User, { + foreignKey: 'userId', + as: 'user', + constraints: false + }) + } + } + }) + return Author +} diff --git a/lib/models/index.js b/lib/models/index.js index e83956e5..96babc2a 100644 --- a/lib/models/index.js +++ b/lib/models/index.js @@ -1,57 +1,55 @@ -"use strict"; - // external modules -var fs = require("fs"); -var path = require("path"); -var Sequelize = require("sequelize"); +var fs = require('fs') +var path = require('path') +var Sequelize = require('sequelize') // core -var config = require('../config.js'); -var logger = require("../logger.js"); +var config = require('../config.js') +var logger = require('../logger.js') -var dbconfig = config.db; -dbconfig.logging = config.debug ? logger.info : false; +var dbconfig = config.db +dbconfig.logging = config.debug ? logger.info : false -var sequelize = null; +var sequelize = null // Heroku specific -if (config.dburl) - sequelize = new Sequelize(config.dburl, dbconfig); -else - sequelize = new Sequelize(dbconfig.database, dbconfig.username, dbconfig.password, dbconfig); +if (config.dburl) { + sequelize = new Sequelize(config.dburl, dbconfig) +} else { + sequelize = new Sequelize(dbconfig.database, dbconfig.username, dbconfig.password, dbconfig) +} // [Postgres] Handling NULL bytes // https://github.com/sequelize/sequelize/issues/6485 -function stripNullByte(value) { - return value ? value.replace(/\u0000/g, "") : value; +function stripNullByte (value) { + return value ? value.replace(/\u0000/g, '') : value } -sequelize.stripNullByte = stripNullByte; +sequelize.stripNullByte = stripNullByte -function processData(data, _default, process) { - if (data === undefined) return data; - else return data === null ? _default : (process ? process(data) : data); +function processData (data, _default, process) { + if (data === undefined) return data + else return data === null ? _default : (process ? process(data) : data) } -sequelize.processData = processData; +sequelize.processData = processData -var db = {}; +var db = {} -fs - .readdirSync(__dirname) +fs.readdirSync(__dirname) .filter(function (file) { - return (file.indexOf(".") !== 0) && (file !== "index.js"); + return (file.indexOf('.') !== 0) && (file !== 'index.js') }) .forEach(function (file) { - var model = sequelize.import(path.join(__dirname, file)); - db[model.name] = model; - }); + var model = sequelize.import(path.join(__dirname, file)) + db[model.name] = model + }) Object.keys(db).forEach(function (modelName) { - if ("associate" in db[modelName]) { - db[modelName].associate(db); - } -}); + if ('associate' in db[modelName]) { + db[modelName].associate(db) + } +}) -db.sequelize = sequelize; -db.Sequelize = Sequelize; +db.sequelize = sequelize +db.Sequelize = Sequelize -module.exports = db; +module.exports = db diff --git a/lib/models/note.js b/lib/models/note.js index 8b38d3f9..bef9ee21 100644 --- a/lib/models/note.js +++ b/lib/models/note.js @@ -1,535 +1,524 @@ -"use strict"; - // external modules -var fs = require('fs'); -var path = require('path'); -var LZString = require('lz-string'); -var md = require('markdown-it')(); -var metaMarked = require('meta-marked'); -var cheerio = require('cheerio'); -var shortId = require('shortid'); -var Sequelize = require("sequelize"); -var async = require('async'); -var moment = require('moment'); -var DiffMatchPatch = require('diff-match-patch'); -var dmp = new DiffMatchPatch(); -var S = require('string'); +var fs = require('fs') +var path = require('path') +var LZString = require('lz-string') +var md = require('markdown-it')() +var metaMarked = require('meta-marked') +var cheerio = require('cheerio') +var shortId = require('shortid') +var Sequelize = require('sequelize') +var async = require('async') +var moment = require('moment') +var DiffMatchPatch = require('diff-match-patch') +var dmp = new DiffMatchPatch() +var S = require('string') // core -var config = require("../config.js"); -var logger = require("../logger.js"); +var config = require('../config.js') +var logger = require('../logger.js') -//ot -var ot = require("../ot/index.js"); +// ot +var ot = require('../ot/index.js') // permission types -var permissionTypes = ["freely", "editable", "limited", "locked", "protected", "private"]; +var permissionTypes = ['freely', 'editable', 'limited', 'locked', 'protected', 'private'] module.exports = function (sequelize, DataTypes) { - var Note = sequelize.define("Note", { - id: { - type: DataTypes.UUID, - primaryKey: true, - defaultValue: Sequelize.UUIDV4 - }, - shortid: { - type: DataTypes.STRING, - unique: true, - allowNull: false, - defaultValue: shortId.generate - }, - alias: { - type: DataTypes.STRING, - unique: true - }, - permission: { - type: DataTypes.ENUM, - values: permissionTypes - }, - viewcount: { - type: DataTypes.INTEGER, - allowNull: false, - defaultValue: 0 - }, - title: { - type: DataTypes.TEXT, - get: function () { - return sequelize.processData(this.getDataValue('title'), ""); - }, - set: function (value) { - this.setDataValue('title', sequelize.stripNullByte(value)); - } - }, - content: { - type: DataTypes.TEXT, - get: function () { - return sequelize.processData(this.getDataValue('content'), ""); - }, - set: function (value) { - this.setDataValue('content', sequelize.stripNullByte(value)); - } - }, - authorship: { - type: DataTypes.TEXT, - get: function () { - return sequelize.processData(this.getDataValue('authorship'), [], JSON.parse); - }, - set: function (value) { - this.setDataValue('authorship', JSON.stringify(value)); - } - }, - lastchangeAt: { - type: DataTypes.DATE - }, - savedAt: { - type: DataTypes.DATE + var Note = sequelize.define('Note', { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: Sequelize.UUIDV4 + }, + shortid: { + type: DataTypes.STRING, + unique: true, + allowNull: false, + defaultValue: shortId.generate + }, + alias: { + type: DataTypes.STRING, + unique: true + }, + permission: { + type: DataTypes.ENUM, + values: permissionTypes + }, + viewcount: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 0 + }, + title: { + type: DataTypes.TEXT, + get: function () { + return sequelize.processData(this.getDataValue('title'), '') + }, + set: function (value) { + this.setDataValue('title', sequelize.stripNullByte(value)) + } + }, + content: { + type: DataTypes.TEXT, + get: function () { + return sequelize.processData(this.getDataValue('content'), '') + }, + set: function (value) { + this.setDataValue('content', sequelize.stripNullByte(value)) + } + }, + authorship: { + type: DataTypes.TEXT, + get: function () { + return sequelize.processData(this.getDataValue('authorship'), [], JSON.parse) + }, + set: function (value) { + this.setDataValue('authorship', JSON.stringify(value)) + } + }, + lastchangeAt: { + type: DataTypes.DATE + }, + savedAt: { + type: DataTypes.DATE + } + }, { + paranoid: true, + classMethods: { + associate: function (models) { + Note.belongsTo(models.User, { + foreignKey: 'ownerId', + as: 'owner', + constraints: false + }) + Note.belongsTo(models.User, { + foreignKey: 'lastchangeuserId', + as: 'lastchangeuser', + constraints: false + }) + Note.hasMany(models.Revision, { + foreignKey: 'noteId', + constraints: false + }) + Note.hasMany(models.Author, { + foreignKey: 'noteId', + as: 'authors', + constraints: false + }) + }, + checkFileExist: function (filePath) { + try { + return fs.statSync(filePath).isFile() + } catch (err) { + return false } - }, { - paranoid: true, - classMethods: { - associate: function (models) { - Note.belongsTo(models.User, { - foreignKey: "ownerId", - as: "owner", - constraints: false - }); - Note.belongsTo(models.User, { - foreignKey: "lastchangeuserId", - as: "lastchangeuser", - constraints: false - }); - Note.hasMany(models.Revision, { - foreignKey: "noteId", - constraints: false - }); - Note.hasMany(models.Author, { - foreignKey: "noteId", - as: "authors", - constraints: false - }); - }, - checkFileExist: function (filePath) { - try { - return fs.statSync(filePath).isFile(); - } catch (err) { - return false; - } - }, - checkNoteIdValid: function (id) { - var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i; - var result = id.match(uuidRegex); - if (result && result.length == 1) - return true; - else - return false; - }, - parseNoteId: function (noteId, callback) { - async.series({ - parseNoteIdByAlias: function (_callback) { - // try to parse note id by alias (e.g. doc) - Note.findOne({ - where: { - alias: noteId - } + }, + checkNoteIdValid: function (id) { + var uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i + var result = id.match(uuidRegex) + if (result && result.length === 1) { return true } else { return false } + }, + parseNoteId: function (noteId, callback) { + async.series({ + parseNoteIdByAlias: function (_callback) { + // try to parse note id by alias (e.g. doc) + Note.findOne({ + where: { + alias: noteId + } + }).then(function (note) { + if (note) { + let filePath = path.join(config.docspath, noteId + '.md') + if (Note.checkFileExist(filePath)) { + // if doc in filesystem have newer modified time than last change time + // then will update the doc in db + var fsModifiedTime = moment(fs.statSync(filePath).mtime) + var dbModifiedTime = moment(note.lastchangeAt || note.createdAt) + var body = fs.readFileSync(filePath, 'utf8') + var contentLength = body.length + var title = Note.parseNoteTitle(body) + if (fsModifiedTime.isAfter(dbModifiedTime) && note.content !== body) { + note.update({ + title: title, + content: body, + lastchangeAt: fsModifiedTime + }).then(function (note) { + sequelize.models.Revision.saveNoteRevision(note, function (err, revision) { + if (err) return _callback(err, null) + // update authorship on after making revision of docs + var patch = dmp.patch_fromText(revision.patch) + var operations = Note.transformPatchToOperations(patch, contentLength) + var authorship = note.authorship + for (let i = 0; i < operations.length; i++) { + authorship = Note.updateAuthorshipByOperation(operations[i], null, authorship) + } + note.update({ + authorship: JSON.stringify(authorship) }).then(function (note) { - if (note) { - var filePath = path.join(config.docspath, noteId + '.md'); - if (Note.checkFileExist(filePath)) { - // if doc in filesystem have newer modified time than last change time - // then will update the doc in db - var fsModifiedTime = moment(fs.statSync(filePath).mtime); - var dbModifiedTime = moment(note.lastchangeAt || note.createdAt); - var body = fs.readFileSync(filePath, 'utf8'); - var contentLength = body.length; - var title = Note.parseNoteTitle(body); - if (fsModifiedTime.isAfter(dbModifiedTime) && note.content !== body) { - note.update({ - title: title, - content: body, - lastchangeAt: fsModifiedTime - }).then(function (note) { - sequelize.models.Revision.saveNoteRevision(note, function (err, revision) { - if (err) return _callback(err, null); - // update authorship on after making revision of docs - var patch = dmp.patch_fromText(revision.patch); - var operations = Note.transformPatchToOperations(patch, contentLength); - var authorship = note.authorship; - for (var i = 0; i < operations.length; i++) { - authorship = Note.updateAuthorshipByOperation(operations[i], null, authorship); - } - note.update({ - authorship: JSON.stringify(authorship) - }).then(function (note) { - return callback(null, note.id); - }).catch(function (err) { - return _callback(err, null); - }); - }); - }).catch(function (err) { - return _callback(err, null); - }); - } else { - return callback(null, note.id); - } - } else { - return callback(null, note.id); - } - } else { - var filePath = path.join(config.docspath, noteId + '.md'); - if (Note.checkFileExist(filePath)) { - Note.create({ - alias: noteId, - owner: null, - permission: 'locked' - }).then(function (note) { - return callback(null, note.id); - }).catch(function (err) { - return _callback(err, null); - }); - } else { - return _callback(null, null); - } - } + return callback(null, note.id) }).catch(function (err) { - return _callback(err, null); - }); - }, - parseNoteIdByLZString: function (_callback) { - // try to parse note id by LZString Base64 - try { - var id = LZString.decompressFromBase64(noteId); - if (id && Note.checkNoteIdValid(id)) - return callback(null, id); - else - return _callback(null, null); - } catch (err) { - return _callback(err, null); - } - }, - parseNoteIdByShortId: function (_callback) { - // try to parse note id by shortId - try { - if (shortId.isValid(noteId)) { - Note.findOne({ - where: { - shortid: noteId - } - }).then(function (note) { - if (!note) return _callback(null, null); - return callback(null, note.id); - }).catch(function (err) { - return _callback(err, null); - }); - } else { - return _callback(null, null); - } - } catch (err) { - return _callback(err, null); - } - } - }, function (err, result) { - if (err) { - logger.error(err); - return callback(err, null); - } - return callback(null, null); - }); - }, - parseNoteInfo: function (body) { - var parsed = Note.extractMeta(body); - var $ = cheerio.load(md.render(parsed.markdown)); - return { - title: Note.extractNoteTitle(parsed.meta, $), - tags: Note.extractNoteTags(parsed.meta, $) - }; - }, - parseNoteTitle: function (body) { - var parsed = Note.extractMeta(body); - var $ = cheerio.load(md.render(parsed.markdown)); - return Note.extractNoteTitle(parsed.meta, $); - }, - extractNoteTitle: function (meta, $) { - var title = ""; - if (meta.title && (typeof meta.title == "string" || typeof meta.title == "number")) { - title = meta.title; + return _callback(err, null) + }) + }) + }).catch(function (err) { + return _callback(err, null) + }) + } else { + return callback(null, note.id) + } } else { - var h1s = $("h1"); - if (h1s.length > 0 && h1s.first().text().split('\n').length == 1) - title = S(h1s.first().text()).stripTags().s; + return callback(null, note.id) } - if (!title) title = "Untitled"; - return title; - }, - generateDescription: function (markdown) { - return markdown.substr(0, 100).replace(/(?:\r\n|\r|\n)/g, ' '); - }, - decodeTitle: function (title) { - return title ? title : 'Untitled'; - }, - generateWebTitle: function (title) { - title = !title || title == "Untitled" ? "HackMD - Collaborative markdown notes" : title + " - HackMD"; - return title; - }, - extractNoteTags: function (meta, $) { - var tags = []; - var rawtags = []; - if (meta.tags && (typeof meta.tags == "string" || typeof meta.tags == "number")) { - var metaTags = ('' + meta.tags).split(','); - for (var i = 0; i < metaTags.length; i++) { - var text = metaTags[i].trim(); - if (text) rawtags.push(text); - } + } else { + var filePath = path.join(config.docspath, noteId + '.md') + if (Note.checkFileExist(filePath)) { + Note.create({ + alias: noteId, + owner: null, + permission: 'locked' + }).then(function (note) { + return callback(null, note.id) + }).catch(function (err) { + return _callback(err, null) + }) } else { - var h6s = $("h6"); - h6s.each(function (key, value) { - if (/^tags/gmi.test($(value).text())) { - var codes = $(value).find("code"); - for (var i = 0; i < codes.length; i++) { - var text = S($(codes[i]).text().trim()).stripTags().s; - if (text) rawtags.push(text); - } - } - }); - } - for (var i = 0; i < rawtags.length; i++) { - var found = false; - for (var j = 0; j < tags.length; j++) { - if (tags[j] == rawtags[i]) { - found = true; - break; - } - } - if (!found) - tags.push(rawtags[i]); - } - return tags; - }, - extractMeta: function (content) { - try { - var obj = metaMarked(content); - if (!obj.markdown) obj.markdown = ""; - if (!obj.meta) obj.meta = {}; - } catch (err) { - var obj = { - markdown: content, - meta: {} - }; - } - return obj; - }, - parseMeta: function (meta) { - var _meta = {}; - if (meta) { - if (meta.title && (typeof meta.title == "string" || typeof meta.title == "number")) - _meta.title = meta.title; - if (meta.description && (typeof meta.description == "string" || typeof meta.description == "number")) - _meta.description = meta.description; - if (meta.robots && (typeof meta.robots == "string" || typeof meta.robots == "number")) - _meta.robots = meta.robots; - if (meta.GA && (typeof meta.GA == "string" || typeof meta.GA == "number")) - _meta.GA = meta.GA; - if (meta.disqus && (typeof meta.disqus == "string" || typeof meta.disqus == "number")) - _meta.disqus = meta.disqus; - if (meta.slideOptions && (typeof meta.slideOptions == "object")) - _meta.slideOptions = meta.slideOptions; + return _callback(null, null) } - return _meta; - }, - updateAuthorshipByOperation: function (operation, userId, authorships) { - var index = 0; - var timestamp = Date.now(); - for (var i = 0; i < operation.length; i++) { - var op = operation[i]; - if (ot.TextOperation.isRetain(op)) { - index += op; - } else if (ot.TextOperation.isInsert(op)) { - var opStart = index; - var opEnd = index + op.length; - var inserted = false; - // authorship format: [userId, startPos, endPos, createdAt, updatedAt] - if (authorships.length <= 0) authorships.push([userId, opStart, opEnd, timestamp, timestamp]); - else { - for (var j = 0; j < authorships.length; j++) { - var authorship = authorships[j]; - if (!inserted) { - var nextAuthorship = authorships[j + 1] || -1; - if (nextAuthorship != -1 && nextAuthorship[1] >= opEnd || j >= authorships.length - 1) { - if (authorship[1] < opStart && authorship[2] > opStart) { - // divide - var postLength = authorship[2] - opStart; - authorship[2] = opStart; - authorship[4] = timestamp; - authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]); - authorships.splice(j + 2, 0, [authorship[0], opEnd, opEnd + postLength, authorship[3], timestamp]); - j += 2; - inserted = true; - } else if (authorship[1] >= opStart) { - authorships.splice(j, 0, [userId, opStart, opEnd, timestamp, timestamp]); - j += 1; - inserted = true; - } else if (authorship[2] <= opStart) { - authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]); - j += 1; - inserted = true; - } - } - } - if (authorship[1] >= opStart) { - authorship[1] += op.length; - authorship[2] += op.length; - } - } - } - index += op.length; - } else if (ot.TextOperation.isDelete(op)) { - var opStart = index; - var opEnd = index - op; - if (operation.length == 1) { - authorships = []; - } else if (authorships.length > 0) { - for (var j = 0; j < authorships.length; j++) { - var authorship = authorships[j]; - if (authorship[1] >= opStart && authorship[1] <= opEnd && authorship[2] >= opStart && authorship[2] <= opEnd) { - authorships.splice(j, 1); - j -= 1; - } else if (authorship[1] < opStart && authorship[1] < opEnd && authorship[2] > opStart && authorship[2] > opEnd) { - authorship[2] += op; - authorship[4] = timestamp; - } else if (authorship[2] >= opStart && authorship[2] <= opEnd) { - authorship[2] = opStart; - authorship[4] = timestamp; - } else if (authorship[1] >= opStart && authorship[1] <= opEnd) { - authorship[1] = opEnd; - authorship[4] = timestamp; - } - if (authorship[1] >= opEnd) { - authorship[1] += op; - authorship[2] += op; - } - } - } - index += op; - } - } - // merge - for (var j = 0; j < authorships.length; j++) { - var authorship = authorships[j]; - for (var k = j + 1; k < authorships.length; k++) { - var nextAuthorship = authorships[k]; - if (nextAuthorship && authorship[0] === nextAuthorship[0] && authorship[2] === nextAuthorship[1]) { - var minTimestamp = Math.min(authorship[3], nextAuthorship[3]); - var maxTimestamp = Math.max(authorship[3], nextAuthorship[3]); - authorships.splice(j, 1, [authorship[0], authorship[1], nextAuthorship[2], minTimestamp, maxTimestamp]); - authorships.splice(k, 1); - j -= 1; - break; - } - } - } - // clear - for (var j = 0; j < authorships.length; j++) { - var authorship = authorships[j]; - if (!authorship[0]) { - authorships.splice(j, 1); - j -= 1; + } + }).catch(function (err) { + return _callback(err, null) + }) + }, + parseNoteIdByLZString: function (_callback) { + // try to parse note id by LZString Base64 + try { + var id = LZString.decompressFromBase64(noteId) + if (id && Note.checkNoteIdValid(id)) { return callback(null, id) } else { return _callback(null, null) } + } catch (err) { + return _callback(err, null) + } + }, + parseNoteIdByShortId: function (_callback) { + // try to parse note id by shortId + try { + if (shortId.isValid(noteId)) { + Note.findOne({ + where: { + shortid: noteId + } + }).then(function (note) { + if (!note) return _callback(null, null) + return callback(null, note.id) + }).catch(function (err) { + return _callback(err, null) + }) + } else { + return _callback(null, null) + } + } catch (err) { + return _callback(err, null) + } + } + }, function (err, result) { + if (err) { + logger.error(err) + return callback(err, null) + } + return callback(null, null) + }) + }, + parseNoteInfo: function (body) { + var parsed = Note.extractMeta(body) + var $ = cheerio.load(md.render(parsed.markdown)) + return { + title: Note.extractNoteTitle(parsed.meta, $), + tags: Note.extractNoteTags(parsed.meta, $) + } + }, + parseNoteTitle: function (body) { + var parsed = Note.extractMeta(body) + var $ = cheerio.load(md.render(parsed.markdown)) + return Note.extractNoteTitle(parsed.meta, $) + }, + extractNoteTitle: function (meta, $) { + var title = '' + if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) { + title = meta.title + } else { + var h1s = $('h1') + if (h1s.length > 0 && h1s.first().text().split('\n').length === 1) { title = S(h1s.first().text()).stripTags().s } + } + if (!title) title = 'Untitled' + return title + }, + generateDescription: function (markdown) { + return markdown.substr(0, 100).replace(/(?:\r\n|\r|\n)/g, ' ') + }, + decodeTitle: function (title) { + return title || 'Untitled' + }, + generateWebTitle: function (title) { + title = !title || title === 'Untitled' ? 'HackMD - Collaborative markdown notes' : title + ' - HackMD' + return title + }, + extractNoteTags: function (meta, $) { + var tags = [] + var rawtags = [] + if (meta.tags && (typeof meta.tags === 'string' || typeof meta.tags === 'number')) { + var metaTags = ('' + meta.tags).split(',') + for (let i = 0; i < metaTags.length; i++) { + var text = metaTags[i].trim() + if (text) rawtags.push(text) + } + } else { + var h6s = $('h6') + h6s.each(function (key, value) { + if (/^tags/gmi.test($(value).text())) { + var codes = $(value).find('code') + for (let i = 0; i < codes.length; i++) { + var text = S($(codes[i]).text().trim()).stripTags().s + if (text) rawtags.push(text) + } + } + }) + } + for (let i = 0; i < rawtags.length; i++) { + var found = false + for (let j = 0; j < tags.length; j++) { + if (tags[j] === rawtags[i]) { + found = true + break + } + } + if (!found) { tags.push(rawtags[i]) } + } + return tags + }, + extractMeta: function (content) { + var obj = null + try { + obj = metaMarked(content) + if (!obj.markdown) obj.markdown = '' + if (!obj.meta) obj.meta = {} + } catch (err) { + obj = { + markdown: content, + meta: {} + } + } + return obj + }, + parseMeta: function (meta) { + var _meta = {} + if (meta) { + if (meta.title && (typeof meta.title === 'string' || typeof meta.title === 'number')) { _meta.title = meta.title } + if (meta.description && (typeof meta.description === 'string' || typeof meta.description === 'number')) { _meta.description = meta.description } + if (meta.robots && (typeof meta.robots === 'string' || typeof meta.robots === 'number')) { _meta.robots = meta.robots } + if (meta.GA && (typeof meta.GA === 'string' || typeof meta.GA === 'number')) { _meta.GA = meta.GA } + if (meta.disqus && (typeof meta.disqus === 'string' || typeof meta.disqus === 'number')) { _meta.disqus = meta.disqus } + if (meta.slideOptions && (typeof meta.slideOptions === 'object')) { _meta.slideOptions = meta.slideOptions } + } + return _meta + }, + updateAuthorshipByOperation: function (operation, userId, authorships) { + var index = 0 + var timestamp = Date.now() + for (let i = 0; i < operation.length; i++) { + var op = operation[i] + if (ot.TextOperation.isRetain(op)) { + index += op + } else if (ot.TextOperation.isInsert(op)) { + let opStart = index + let opEnd = index + op.length + var inserted = false + // authorship format: [userId, startPos, endPos, createdAt, updatedAt] + if (authorships.length <= 0) authorships.push([userId, opStart, opEnd, timestamp, timestamp]) + else { + for (let j = 0; j < authorships.length; j++) { + let authorship = authorships[j] + if (!inserted) { + let nextAuthorship = authorships[j + 1] || -1 + if ((nextAuthorship !== -1 && nextAuthorship[1] >= opEnd) || j >= authorships.length - 1) { + if (authorship[1] < opStart && authorship[2] > opStart) { + // divide + let postLength = authorship[2] - opStart + authorship[2] = opStart + authorship[4] = timestamp + authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]) + authorships.splice(j + 2, 0, [authorship[0], opEnd, opEnd + postLength, authorship[3], timestamp]) + j += 2 + inserted = true + } else if (authorship[1] >= opStart) { + authorships.splice(j, 0, [userId, opStart, opEnd, timestamp, timestamp]) + j += 1 + inserted = true + } else if (authorship[2] <= opStart) { + authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]) + j += 1 + inserted = true } + } } - return authorships; - }, - transformPatchToOperations: function (patch, contentLength) { - var operations = []; - if (patch.length > 0) { - // calculate original content length - for (var j = patch.length - 1; j >= 0; j--) { - var p = patch[j]; - for (var i = 0; i < p.diffs.length; i++) { - var diff = p.diffs[i]; - switch(diff[0]) { - case 1: // insert - contentLength -= diff[1].length; - break; - case -1: // delete - contentLength += diff[1].length; - break; - } - } - } - // generate operations - var bias = 0; - var lengthBias = 0; - for (var j = 0; j < patch.length; j++) { - var operation = []; - var p = patch[j]; - var currIndex = p.start1; - var currLength = contentLength - bias; - for (var i = 0; i < p.diffs.length; i++) { - var diff = p.diffs[i]; - switch(diff[0]) { - case 0: // retain - if (i == 0) // first - operation.push(currIndex + diff[1].length); - else if (i != p.diffs.length - 1) // mid - operation.push(diff[1].length); - else // last - operation.push(currLength + lengthBias - currIndex); - currIndex += diff[1].length; - break; - case 1: // insert - operation.push(diff[1]); - lengthBias += diff[1].length; - currIndex += diff[1].length; - break; - case -1: // delete - operation.push(-diff[1].length); - bias += diff[1].length; - currIndex += diff[1].length; - break; - } - } - operations.push(operation); - } + if (authorship[1] >= opStart) { + authorship[1] += op.length + authorship[2] += op.length } - return operations; + } } - }, - hooks: { - beforeCreate: function (note, options, callback) { - // if no content specified then use default note - if (!note.content) { - var body = null; - var filePath = null; - if (!note.alias) { - filePath = config.defaultnotepath; - } else { - filePath = path.join(config.docspath, note.alias + '.md'); - } - if (Note.checkFileExist(filePath)) { - var fsCreatedTime = moment(fs.statSync(filePath).ctime); - body = fs.readFileSync(filePath, 'utf8'); - note.title = Note.parseNoteTitle(body); - note.content = body; - if (filePath !== config.defaultnotepath) { - note.createdAt = fsCreatedTime; - } - } + index += op.length + } else if (ot.TextOperation.isDelete(op)) { + let opStart = index + let opEnd = index - op + if (operation.length === 1) { + authorships = [] + } else if (authorships.length > 0) { + for (let j = 0; j < authorships.length; j++) { + let authorship = authorships[j] + if (authorship[1] >= opStart && authorship[1] <= opEnd && authorship[2] >= opStart && authorship[2] <= opEnd) { + authorships.splice(j, 1) + j -= 1 + } else if (authorship[1] < opStart && authorship[1] < opEnd && authorship[2] > opStart && authorship[2] > opEnd) { + authorship[2] += op + authorship[4] = timestamp + } else if (authorship[2] >= opStart && authorship[2] <= opEnd) { + authorship[2] = opStart + authorship[4] = timestamp + } else if (authorship[1] >= opStart && authorship[1] <= opEnd) { + authorship[1] = opEnd + authorship[4] = timestamp } - // 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 = config.defaultpermission; - } else { - note.permission = "freely"; - } + if (authorship[1] >= opEnd) { + authorship[1] += op + authorship[2] += op } - return callback(null, note); - }, - afterCreate: function (note, options, callback) { - sequelize.models.Revision.saveNoteRevision(note, function (err, revision) { - callback(err, note); - }); + } + } + index += op + } + } + // merge + for (let j = 0; j < authorships.length; j++) { + let authorship = authorships[j] + for (let k = j + 1; k < authorships.length; k++) { + let nextAuthorship = authorships[k] + if (nextAuthorship && authorship[0] === nextAuthorship[0] && authorship[2] === nextAuthorship[1]) { + let minTimestamp = Math.min(authorship[3], nextAuthorship[3]) + let maxTimestamp = Math.max(authorship[3], nextAuthorship[3]) + authorships.splice(j, 1, [authorship[0], authorship[1], nextAuthorship[2], minTimestamp, maxTimestamp]) + authorships.splice(k, 1) + j -= 1 + break + } + } + } + // clear + for (let j = 0; j < authorships.length; j++) { + let authorship = authorships[j] + if (!authorship[0]) { + authorships.splice(j, 1) + j -= 1 + } + } + return authorships + }, + transformPatchToOperations: function (patch, contentLength) { + var operations = [] + if (patch.length > 0) { + // calculate original content length + for (let j = patch.length - 1; j >= 0; j--) { + var p = patch[j] + for (let i = 0; i < p.diffs.length; i++) { + var diff = p.diffs[i] + switch (diff[0]) { + case 1: // insert + contentLength -= diff[1].length + break + case -1: // delete + contentLength += diff[1].length + break + } + } + } + // generate operations + var bias = 0 + var lengthBias = 0 + for (let j = 0; j < patch.length; j++) { + var operation = [] + let p = patch[j] + var currIndex = p.start1 + var currLength = contentLength - bias + for (let i = 0; i < p.diffs.length; i++) { + let diff = p.diffs[i] + switch (diff[0]) { + case 0: // retain + if (i === 0) { + // first + operation.push(currIndex + diff[1].length) + } else if (i !== p.diffs.length - 1) { + // mid + operation.push(diff[1].length) + } else { + // last + operation.push(currLength + lengthBias - currIndex) + } + currIndex += diff[1].length + break + case 1: // insert + operation.push(diff[1]) + lengthBias += diff[1].length + currIndex += diff[1].length + break + case -1: // delete + operation.push(-diff[1].length) + bias += diff[1].length + currIndex += diff[1].length + break + } } + operations.push(operation) + } + } + return operations + } + }, + hooks: { + beforeCreate: function (note, options, callback) { + // if no content specified then use default note + if (!note.content) { + var body = null + let filePath = null + if (!note.alias) { + filePath = config.defaultnotepath + } else { + filePath = path.join(config.docspath, note.alias + '.md') + } + if (Note.checkFileExist(filePath)) { + var fsCreatedTime = moment(fs.statSync(filePath).ctime) + body = fs.readFileSync(filePath, 'utf8') + note.title = Note.parseNoteTitle(body) + note.content = body + if (filePath !== config.defaultnotepath) { + note.createdAt = fsCreatedTime + } + } + } + // 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 = config.defaultpermission + } else { + note.permission = 'freely' + } } - }); + return callback(null, note) + }, + afterCreate: function (note, options, callback) { + sequelize.models.Revision.saveNoteRevision(note, function (err, revision) { + callback(err, note) + }) + } + } + }) - return Note; -}; + return Note +} diff --git a/lib/models/revision.js b/lib/models/revision.js index c7360fed..d8dab30a 100644 --- a/lib/models/revision.js +++ b/lib/models/revision.js @@ -1,306 +1,306 @@ -"use strict"; - // external modules -var Sequelize = require("sequelize"); -var async = require('async'); -var moment = require('moment'); -var childProcess = require('child_process'); -var shortId = require('shortid'); +var Sequelize = require('sequelize') +var async = require('async') +var moment = require('moment') +var childProcess = require('child_process') +var shortId = require('shortid') // core -var config = require("../config.js"); -var logger = require("../logger.js"); +var config = require('../config.js') +var logger = require('../logger.js') -var dmpWorker = createDmpWorker(); -var dmpCallbackCache = {}; +var dmpWorker = createDmpWorker() +var dmpCallbackCache = {} -function createDmpWorker() { - var worker = childProcess.fork("./lib/workers/dmpWorker.js", { - stdio: 'ignore' - }); - if (config.debug) logger.info('dmp worker process started'); - worker.on('message', function (data) { - if (!data || !data.msg || !data.cacheKey) { - return logger.error('dmp worker error: not enough data on message'); - } - var cacheKey = data.cacheKey; - switch(data.msg) { - case 'error': - dmpCallbackCache[cacheKey](data.error, null); - break; - case 'check': - dmpCallbackCache[cacheKey](null, data.result); - break; - } - delete dmpCallbackCache[cacheKey]; - }); - worker.on('close', function (code) { - dmpWorker = null; - if (config.debug) logger.info('dmp worker process exited with code ' + code); - }); - return worker; +function createDmpWorker () { + var worker = childProcess.fork('./lib/workers/dmpWorker.js', { + stdio: 'ignore' + }) + if (config.debug) logger.info('dmp worker process started') + worker.on('message', function (data) { + if (!data || !data.msg || !data.cacheKey) { + return logger.error('dmp worker error: not enough data on message') + } + var cacheKey = data.cacheKey + switch (data.msg) { + case 'error': + dmpCallbackCache[cacheKey](data.error, null) + break + case 'check': + dmpCallbackCache[cacheKey](null, data.result) + break + } + delete dmpCallbackCache[cacheKey] + }) + worker.on('close', function (code) { + dmpWorker = null + if (config.debug) logger.info('dmp worker process exited with code ' + code) + }) + return worker } -function sendDmpWorker(data, callback) { - if (!dmpWorker) dmpWorker = createDmpWorker(); - var cacheKey = Date.now() + '_' + shortId.generate(); - dmpCallbackCache[cacheKey] = callback; - data = Object.assign(data, { - cacheKey: cacheKey - }); - dmpWorker.send(data); +function sendDmpWorker (data, callback) { + if (!dmpWorker) dmpWorker = createDmpWorker() + var cacheKey = Date.now() + '_' + shortId.generate() + dmpCallbackCache[cacheKey] = callback + data = Object.assign(data, { + cacheKey: cacheKey + }) + dmpWorker.send(data) } module.exports = function (sequelize, DataTypes) { - var Revision = sequelize.define("Revision", { - id: { - type: DataTypes.UUID, - primaryKey: true, - defaultValue: Sequelize.UUIDV4 - }, - patch: { - type: DataTypes.TEXT, - get: function () { - return sequelize.processData(this.getDataValue('patch'), ""); - }, - set: function (value) { - this.setDataValue('patch', sequelize.stripNullByte(value)); - } - }, - lastContent: { - type: DataTypes.TEXT, - get: function () { - return sequelize.processData(this.getDataValue('lastContent'), ""); + var Revision = sequelize.define('Revision', { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: Sequelize.UUIDV4 + }, + patch: { + type: DataTypes.TEXT, + get: function () { + return sequelize.processData(this.getDataValue('patch'), '') + }, + set: function (value) { + this.setDataValue('patch', sequelize.stripNullByte(value)) + } + }, + lastContent: { + type: DataTypes.TEXT, + get: function () { + return sequelize.processData(this.getDataValue('lastContent'), '') + }, + set: function (value) { + this.setDataValue('lastContent', sequelize.stripNullByte(value)) + } + }, + content: { + type: DataTypes.TEXT, + get: function () { + return sequelize.processData(this.getDataValue('content'), '') + }, + set: function (value) { + this.setDataValue('content', sequelize.stripNullByte(value)) + } + }, + length: { + type: DataTypes.INTEGER + }, + authorship: { + type: DataTypes.TEXT, + get: function () { + return sequelize.processData(this.getDataValue('authorship'), [], JSON.parse) + }, + set: function (value) { + this.setDataValue('authorship', value ? JSON.stringify(value) : value) + } + } + }, { + classMethods: { + associate: function (models) { + Revision.belongsTo(models.Note, { + foreignKey: 'noteId', + as: 'note', + constraints: false + }) + }, + getNoteRevisions: function (note, callback) { + Revision.findAll({ + where: { + noteId: note.id + }, + order: '"createdAt" DESC' + }).then(function (revisions) { + var data = [] + for (var i = 0, l = revisions.length; i < l; i++) { + var revision = revisions[i] + data.push({ + time: moment(revision.createdAt).valueOf(), + length: revision.length + }) + } + callback(null, data) + }).catch(function (err) { + callback(err, null) + }) + }, + getPatchedNoteRevisionByTime: function (note, time, callback) { + // find all revisions to prepare for all possible calculation + Revision.findAll({ + where: { + noteId: note.id + }, + order: '"createdAt" DESC' + }).then(function (revisions) { + if (revisions.length <= 0) return callback(null, null) + // measure target revision position + Revision.count({ + where: { + noteId: note.id, + createdAt: { + $gte: time + } }, - set: function (value) { - this.setDataValue('lastContent', sequelize.stripNullByte(value)); + order: '"createdAt" DESC' + }).then(function (count) { + if (count <= 0) return callback(null, null) + sendDmpWorker({ + msg: 'get revision', + revisions: revisions, + count: count + }, callback) + }).catch(function (err) { + return callback(err, null) + }) + }).catch(function (err) { + return callback(err, null) + }) + }, + checkAllNotesRevision: function (callback) { + Revision.saveAllNotesRevision(function (err, notes) { + if (err) return callback(err, null) + if (!notes || notes.length <= 0) { + return callback(null, notes) + } else { + Revision.checkAllNotesRevision(callback) + } + }) + }, + saveAllNotesRevision: function (callback) { + sequelize.models.Note.findAll({ + // query all notes that need to save for revision + where: { + $and: [ + { + lastchangeAt: { + $or: { + $eq: null, + $and: { + $ne: null, + $gt: sequelize.col('createdAt') + } + } + } + }, + { + savedAt: { + $or: { + $eq: null, + $lt: sequelize.col('lastchangeAt') + } + } + } + ] + } + }).then(function (notes) { + if (notes.length <= 0) return callback(null, notes) + var savedNotes = [] + async.each(notes, function (note, _callback) { + // revision saving policy: note not been modified for 5 mins or not save for 10 mins + if (note.lastchangeAt && note.savedAt) { + var lastchangeAt = moment(note.lastchangeAt) + var savedAt = moment(note.savedAt) + if (moment().isAfter(lastchangeAt.add(5, 'minutes'))) { + savedNotes.push(note) + Revision.saveNoteRevision(note, _callback) + } else if (lastchangeAt.isAfter(savedAt.add(10, 'minutes'))) { + savedNotes.push(note) + Revision.saveNoteRevision(note, _callback) + } else { + return _callback(null, null) + } + } else { + savedNotes.push(note) + Revision.saveNoteRevision(note, _callback) } - }, - content: { - type: DataTypes.TEXT, - get: function () { - return sequelize.processData(this.getDataValue('content'), ""); - }, - set: function (value) { - this.setDataValue('content', sequelize.stripNullByte(value)); + }, function (err) { + if (err) { + return callback(err, null) } - }, - length: { - type: DataTypes.INTEGER - }, - authorship: { - type: DataTypes.TEXT, - get: function () { - return sequelize.processData(this.getDataValue('authorship'), [], JSON.parse); - }, - set: function (value) { - this.setDataValue('authorship', value ? JSON.stringify(value) : value); - } - } - }, { - classMethods: { - associate: function (models) { - Revision.belongsTo(models.Note, { - foreignKey: "noteId", - as: "note", - constraints: false - }); - }, - getNoteRevisions: function (note, callback) { - Revision.findAll({ - where: { - noteId: note.id - }, - order: '"createdAt" DESC' - }).then(function (revisions) { - var data = []; - for (var i = 0, l = revisions.length; i < l; i++) { - var revision = revisions[i]; - data.push({ - time: moment(revision.createdAt).valueOf(), - length: revision.length - }); - } - callback(null, data); - }).catch(function (err) { - callback(err, null); - }); - }, - getPatchedNoteRevisionByTime: function (note, time, callback) { - // find all revisions to prepare for all possible calculation - Revision.findAll({ - where: { - noteId: note.id - }, - order: '"createdAt" DESC' - }).then(function (revisions) { - if (revisions.length <= 0) return callback(null, null); - // measure target revision position - Revision.count({ - where: { - noteId: note.id, - createdAt: { - $gte: time - } - }, - order: '"createdAt" DESC' - }).then(function (count) { - if (count <= 0) return callback(null, null); - sendDmpWorker({ - msg: 'get revision', - revisions: revisions, - count: count - }, callback); - }).catch(function (err) { - return callback(err, null); - }); + // return null when no notes need saving at this moment but have delayed tasks to be done + var result = ((savedNotes.length === 0) && (notes.length > savedNotes.length)) ? null : savedNotes + return callback(null, result) + }) + }).catch(function (err) { + return callback(err, null) + }) + }, + saveNoteRevision: function (note, callback) { + Revision.findAll({ + where: { + noteId: note.id + }, + order: '"createdAt" DESC' + }).then(function (revisions) { + if (revisions.length <= 0) { + // if no revision available + Revision.create({ + noteId: note.id, + lastContent: note.content, + length: note.content.length, + authorship: note.authorship + }).then(function (revision) { + Revision.finishSaveNoteRevision(note, revision, callback) + }).catch(function (err) { + return callback(err, null) + }) + } else { + var latestRevision = revisions[0] + var lastContent = latestRevision.content || latestRevision.lastContent + var content = note.content + sendDmpWorker({ + msg: 'create patch', + lastDoc: lastContent, + currDoc: content + }, function (err, patch) { + if (err) logger.error('save note revision error', err) + if (!patch) { + // if patch is empty (means no difference) then just update the latest revision updated time + latestRevision.changed('updatedAt', true) + latestRevision.update({ + updatedAt: Date.now() + }).then(function (revision) { + Revision.finishSaveNoteRevision(note, revision, callback) }).catch(function (err) { - return callback(err, null); - }); - }, - checkAllNotesRevision: function (callback) { - Revision.saveAllNotesRevision(function (err, notes) { - if (err) return callback(err, null); - if (!notes || notes.length <= 0) { - return callback(null, notes); - } else { - Revision.checkAllNotesRevision(callback); - } - }); - }, - saveAllNotesRevision: function (callback) { - sequelize.models.Note.findAll({ - // query all notes that need to save for revision - where: { - $and: [ - { - lastchangeAt: { - $or: { - $eq: null, - $and: { - $ne: null, - $gt: sequelize.col('createdAt') - } - } - } - }, - { - savedAt: { - $or: { - $eq: null, - $lt: sequelize.col('lastchangeAt') - } - } - } - ] - } - }).then(function (notes) { - if (notes.length <= 0) return callback(null, notes); - var savedNotes = []; - async.each(notes, function (note, _callback) { - // revision saving policy: note not been modified for 5 mins or not save for 10 mins - if (note.lastchangeAt && note.savedAt) { - var lastchangeAt = moment(note.lastchangeAt); - var savedAt = moment(note.savedAt); - if (moment().isAfter(lastchangeAt.add(5, 'minutes'))) { - savedNotes.push(note); - Revision.saveNoteRevision(note, _callback); - } else if (lastchangeAt.isAfter(savedAt.add(10, 'minutes'))) { - savedNotes.push(note); - Revision.saveNoteRevision(note, _callback); - } else { - return _callback(null, null); - } - } else { - savedNotes.push(note); - Revision.saveNoteRevision(note, _callback); - } - }, function (err) { - if (err) return callback(err, null); - // return null when no notes need saving at this moment but have delayed tasks to be done - var result = ((savedNotes.length == 0) && (notes.length > savedNotes.length)) ? null : savedNotes; - return callback(null, result); - }); + return callback(err, null) + }) + } else { + Revision.create({ + noteId: note.id, + patch: patch, + content: note.content, + length: note.content.length, + authorship: note.authorship + }).then(function (revision) { + // clear last revision content to reduce db size + latestRevision.update({ + content: null + }).then(function () { + Revision.finishSaveNoteRevision(note, revision, callback) + }).catch(function (err) { + return callback(err, null) + }) }).catch(function (err) { - return callback(err, null); - }); - }, - saveNoteRevision: function (note, callback) { - Revision.findAll({ - where: { - noteId: note.id - }, - order: '"createdAt" DESC' - }).then(function (revisions) { - if (revisions.length <= 0) { - // if no revision available - Revision.create({ - noteId: note.id, - lastContent: note.content, - length: note.content.length, - authorship: note.authorship - }).then(function (revision) { - Revision.finishSaveNoteRevision(note, revision, callback); - }).catch(function (err) { - return callback(err, null); - }); - } else { - var latestRevision = revisions[0]; - var lastContent = latestRevision.content || latestRevision.lastContent; - var content = note.content; - sendDmpWorker({ - msg: 'create patch', - lastDoc: lastContent, - currDoc: content, - }, function (err, patch) { - if (err) logger.error('save note revision error', err); - if (!patch) { - // if patch is empty (means no difference) then just update the latest revision updated time - latestRevision.changed('updatedAt', true); - latestRevision.update({ - updatedAt: Date.now() - }).then(function (revision) { - Revision.finishSaveNoteRevision(note, revision, callback); - }).catch(function (err) { - return callback(err, null); - }); - } else { - Revision.create({ - noteId: note.id, - patch: patch, - content: note.content, - length: note.content.length, - authorship: note.authorship - }).then(function (revision) { - // clear last revision content to reduce db size - latestRevision.update({ - content: null - }).then(function () { - Revision.finishSaveNoteRevision(note, revision, callback); - }).catch(function (err) { - return callback(err, null); - }); - }).catch(function (err) { - return callback(err, null); - }); - } - }); - } - }).catch(function (err) { - return callback(err, null); - }); - }, - finishSaveNoteRevision: function (note, revision, callback) { - note.update({ - savedAt: revision.updatedAt - }).then(function () { - return callback(null, revision); - }).catch(function (err) { - return callback(err, null); - }); - } - } - }); + return callback(err, null) + }) + } + }) + } + }).catch(function (err) { + return callback(err, null) + }) + }, + finishSaveNoteRevision: function (note, revision, callback) { + note.update({ + savedAt: revision.updatedAt + }).then(function () { + return callback(null, revision) + }).catch(function (err) { + return callback(err, null) + }) + } + } + }) - return Revision; -};
\ No newline at end of file + return Revision +} diff --git a/lib/models/temp.js b/lib/models/temp.js index 6eeff153..e770bb3a 100644 --- a/lib/models/temp.js +++ b/lib/models/temp.js @@ -1,19 +1,17 @@ -"use strict"; - -//external modules -var shortId = require('shortid'); +// external modules +var shortId = require('shortid') module.exports = function (sequelize, DataTypes) { - var Temp = sequelize.define("Temp", { - id: { - type: DataTypes.STRING, - primaryKey: true, - defaultValue: shortId.generate - }, - data: { - type: DataTypes.TEXT - } - }); - - return Temp; -};
\ No newline at end of file + var Temp = sequelize.define('Temp', { + id: { + type: DataTypes.STRING, + primaryKey: true, + defaultValue: shortId.generate + }, + data: { + type: DataTypes.TEXT + } + }) + + return Temp +} diff --git a/lib/models/user.js b/lib/models/user.js index dd93bf78..f7e533b7 100644 --- a/lib/models/user.js +++ b/lib/models/user.js @@ -1,149 +1,147 @@ -"use strict"; - // external modules -var md5 = require("blueimp-md5"); -var Sequelize = require("sequelize"); -var scrypt = require('scrypt'); +var md5 = require('blueimp-md5') +var Sequelize = require('sequelize') +var scrypt = require('scrypt') // core -var logger = require("../logger.js"); -var letterAvatars = require('../letter-avatars.js'); +var logger = require('../logger.js') +var letterAvatars = require('../letter-avatars.js') module.exports = function (sequelize, DataTypes) { - var User = sequelize.define("User", { - id: { - type: DataTypes.UUID, - primaryKey: true, - defaultValue: Sequelize.UUIDV4 - }, - profileid: { - type: DataTypes.STRING, - unique: true - }, - profile: { - type: DataTypes.TEXT - }, - history: { - type: DataTypes.TEXT - }, - accessToken: { - type: DataTypes.STRING - }, - refreshToken: { - type: DataTypes.STRING - }, - email: { - type: Sequelize.TEXT, - validate: { - isEmail: true - } - }, - password: { - type: Sequelize.TEXT, - set: function(value) { - var hash = scrypt.kdfSync(value, scrypt.paramsSync(0.1)).toString("hex"); - this.setDataValue('password', hash); - } + var User = sequelize.define('User', { + id: { + type: DataTypes.UUID, + primaryKey: true, + defaultValue: Sequelize.UUIDV4 + }, + profileid: { + type: DataTypes.STRING, + unique: true + }, + profile: { + type: DataTypes.TEXT + }, + history: { + type: DataTypes.TEXT + }, + accessToken: { + type: DataTypes.STRING + }, + refreshToken: { + type: DataTypes.STRING + }, + email: { + type: Sequelize.TEXT, + validate: { + isEmail: true + } + }, + password: { + type: Sequelize.TEXT, + set: function (value) { + var hash = scrypt.kdfSync(value, scrypt.paramsSync(0.1)).toString('hex') + this.setDataValue('password', hash) + } + } + }, { + instanceMethods: { + verifyPassword: function (attempt) { + if (scrypt.verifyKdfSync(new Buffer(this.password, 'hex'), attempt)) { + return this + } else { + return false } - }, { - instanceMethods: { - verifyPassword: function(attempt) { - if (scrypt.verifyKdfSync(new Buffer(this.password, "hex"), attempt)) { - return this; - } else { - return false; - } - } - }, - classMethods: { - associate: function (models) { - User.hasMany(models.Note, { - foreignKey: "ownerId", - constraints: false - }); - User.hasMany(models.Note, { - foreignKey: "lastchangeuserId", - constraints: false - }); - }, - getProfile: function (user) { - return user.profile ? User.parseProfile(user.profile) : (user.email ? User.parseProfileByEmail(user.email) : null); - }, - parseProfile: function (profile) { - try { - var profile = JSON.parse(profile); - } catch (err) { - logger.error(err); - profile = null; - } - if (profile) { - profile = { - name: profile.displayName || profile.username, - photo: User.parsePhotoByProfile(profile), - biggerphoto: User.parsePhotoByProfile(profile, true) - } - } - return profile; - }, - parsePhotoByProfile: function (profile, bigger) { - var photo = null; - switch (profile.provider) { - case "facebook": - 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'; - if (bigger) photo += '?size=original'; - else photo += '?size=bigger'; - break; - case "github": - photo = 'https://avatars.githubusercontent.com/u/' + profile.id; - if (bigger) photo += '?s=400'; - else photo += '?s=96'; - break; - case "gitlab": - 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); - if (bigger) photo += '?s=400'; - else photo += '?s=96'; - break; - case "google": - 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]); - if (bigger) photo += '?s=400'; - else photo += '?s=96'; - } else { - photo = letterAvatars(profile.username); - } - break; - } - return photo; - }, - parseProfileByEmail: function (email) { - var photoUrl = 'https://www.gravatar.com/avatar/' + md5(email); - return { - name: email.substring(0, email.lastIndexOf("@")), - photo: photoUrl += '?s=96', - biggerphoto: photoUrl += '?s=400' - }; + } + }, + classMethods: { + associate: function (models) { + User.hasMany(models.Note, { + foreignKey: 'ownerId', + constraints: false + }) + User.hasMany(models.Note, { + foreignKey: 'lastchangeuserId', + constraints: false + }) + }, + getProfile: function (user) { + return user.profile ? User.parseProfile(user.profile) : (user.email ? User.parseProfileByEmail(user.email) : null) + }, + parseProfile: function (profile) { + try { + profile = JSON.parse(profile) + } catch (err) { + logger.error(err) + profile = null + } + if (profile) { + profile = { + name: profile.displayName || profile.username, + photo: User.parsePhotoByProfile(profile), + biggerphoto: User.parsePhotoByProfile(profile, true) + } + } + return profile + }, + parsePhotoByProfile: function (profile, bigger) { + var photo = null + switch (profile.provider) { + case 'facebook': + 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' + if (bigger) photo += '?size=original' + else photo += '?size=bigger' + break + case 'github': + photo = 'https://avatars.githubusercontent.com/u/' + profile.id + if (bigger) photo += '?s=400' + else photo += '?s=96' + break + case 'gitlab': + 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) + if (bigger) photo += '?s=400' + else photo += '?s=96' + break + case 'google': + 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]) + if (bigger) photo += '?s=400' + else photo += '?s=96' + } else { + photo = letterAvatars(profile.username) } + break + } + return photo + }, + parseProfileByEmail: function (email) { + var photoUrl = 'https://www.gravatar.com/avatar/' + md5(email) + return { + name: email.substring(0, email.lastIndexOf('@')), + photo: photoUrl + '?s=96', + biggerphoto: photoUrl + '?s=400' } - }); + } + } + }) - return User; -};
\ No newline at end of file + return User +} diff --git a/lib/realtime.js b/lib/realtime.js index c1db6886..cff795c7 100644 --- a/lib/realtime.js +++ b/lib/realtime.js @@ -1,937 +1,924 @@ -//realtime -//external modules -var cookie = require('cookie'); -var cookieParser = require('cookie-parser'); -var url = require('url'); -var async = require('async'); -var LZString = require('lz-string'); -var randomcolor = require("randomcolor"); -var Chance = require('chance'), - chance = new Chance(); -var moment = require('moment'); - -//core -var config = require("./config.js"); -var logger = require("./logger.js"); -var history = require("./history.js"); -var models = require("./models"); - -//ot -var ot = require("./ot/index.js"); - -//public +// realtime +// external modules +var cookie = require('cookie') +var cookieParser = require('cookie-parser') +var url = require('url') +var async = require('async') +var LZString = require('lz-string') +var randomcolor = require('randomcolor') +var Chance = require('chance') +var chance = new Chance() +var moment = require('moment') + +// core +var config = require('./config.js') +var logger = require('./logger.js') +var history = require('./history.js') +var models = require('./models') + +// ot +var ot = require('./ot/index.js') + +// public var realtime = { - io: null, - onAuthorizeSuccess: onAuthorizeSuccess, - onAuthorizeFail: onAuthorizeFail, - secure: secure, - connection: connection, - getStatus: getStatus, - isReady: isReady -}; - -function onAuthorizeSuccess(data, accept) { - accept(); + io: null, + onAuthorizeSuccess: onAuthorizeSuccess, + onAuthorizeFail: onAuthorizeFail, + secure: secure, + connection: connection, + getStatus: getStatus, + isReady: isReady } -function onAuthorizeFail(data, message, error, accept) { - accept(); //accept whether authorize or not to allow anonymous usage +function onAuthorizeSuccess (data, accept) { + accept() } -//secure the origin by the cookie -function secure(socket, next) { - try { - var handshakeData = socket.request; - if (handshakeData.headers.cookie) { - handshakeData.cookie = cookie.parse(handshakeData.headers.cookie); - handshakeData.sessionID = cookieParser.signedCookie(handshakeData.cookie[config.sessionname], config.sessionsecret); - if (handshakeData.sessionID && +function onAuthorizeFail (data, message, error, accept) { + accept() // accept whether authorize or not to allow anonymous usage +} + +// secure the origin by the cookie +function secure (socket, next) { + try { + var handshakeData = socket.request + if (handshakeData.headers.cookie) { + handshakeData.cookie = cookie.parse(handshakeData.headers.cookie) + handshakeData.sessionID = cookieParser.signedCookie(handshakeData.cookie[config.sessionname], config.sessionsecret) + if (handshakeData.sessionID && handshakeData.cookie[config.sessionname] && - handshakeData.cookie[config.sessionname] != handshakeData.sessionID) { - if (config.debug) - logger.info("AUTH success cookie: " + handshakeData.sessionID); - return next(); - } else { - next(new Error('AUTH failed: Cookie is invalid.')); - } - } else { - next(new Error('AUTH failed: No cookie transmitted.')); - } - } catch (ex) { - next(new Error("AUTH failed:" + JSON.stringify(ex))); + handshakeData.cookie[config.sessionname] !== handshakeData.sessionID) { + if (config.debug) { logger.info('AUTH success cookie: ' + handshakeData.sessionID) } + return next() + } else { + next(new Error('AUTH failed: Cookie is invalid.')) + } + } else { + next(new Error('AUTH failed: No cookie transmitted.')) } + } catch (ex) { + next(new Error('AUTH failed:' + JSON.stringify(ex))) + } } -function emitCheck(note) { - var out = { - title: note.title, - updatetime: note.updatetime, - lastchangeuser: note.lastchangeuser, - lastchangeuserprofile: note.lastchangeuserprofile, - authors: note.authors, - authorship: note.authorship - }; - realtime.io.to(note.id).emit('check', out); +function emitCheck (note) { + var out = { + title: note.title, + updatetime: note.updatetime, + lastchangeuser: note.lastchangeuser, + lastchangeuserprofile: note.lastchangeuserprofile, + authors: note.authors, + authorship: note.authorship + } + realtime.io.to(note.id).emit('check', out) } -//actions -var users = {}; -var notes = {}; -//update when the note is dirty -var updater = setInterval(function () { - async.each(Object.keys(notes), function (key, callback) { - var note = notes[key]; - if (note.server.isDirty) { - if (config.debug) logger.info("updater found dirty note: " + key); - note.server.isDirty = false; - updateNote(note, function(err, _note) { - // handle when note already been clean up - if (!notes[key] || !notes[key].server) return callback(null, null); - if (!_note) { - realtime.io.to(note.id).emit('info', { - code: 404 - }); - logger.error('note not found: ', note.id); - } - if (err || !_note) { - for (var i = 0, l = note.socks.length; i < l; i++) { - var sock = note.socks[i]; - if (typeof sock !== 'undefined' && sock) { - setTimeout(function () { - sock.disconnect(true); - }, 0); - } - } - return callback(err, null); - } - note.updatetime = moment(_note.lastchangeAt).valueOf(); - emitCheck(note); - return callback(null, null); - }); - } else { - return callback(null, null); - } - }, function (err) { - if (err) return logger.error('updater error', err); - }); -}, 1000); -function updateNote(note, callback) { - models.Note.findOne({ - where: { - id: note.id +// actions +var users = {} +var notes = {} +// update when the note is dirty +setInterval(function () { + async.each(Object.keys(notes), function (key, callback) { + var note = notes[key] + if (note.server.isDirty) { + if (config.debug) logger.info('updater found dirty note: ' + key) + note.server.isDirty = false + updateNote(note, function (err, _note) { + // handle when note already been clean up + if (!notes[key] || !notes[key].server) return callback(null, null) + if (!_note) { + realtime.io.to(note.id).emit('info', { + code: 404 + }) + logger.error('note not found: ', note.id) } - }).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({ - where: { - id: note.lastchangeuser - } - }).then(function (user) { - if (!user) return callback(null, null); - note.lastchangeuserprofile = models.User.getProfile(user); - return finishUpdateNote(note, _note, callback); - }).catch(function (err) { - logger.error(err); - return callback(err, null); - }); - } else { - return finishUpdateNote(note, _note, callback); + if (err || !_note) { + for (var i = 0, l = note.socks.length; i < l; i++) { + var sock = note.socks[i] + if (typeof sock !== 'undefined' && sock) { + setTimeout(function () { + sock.disconnect(true) + }, 0) } - } else { - note.lastchangeuserprofile = null; - return finishUpdateNote(note, _note, callback); + } + return callback(err, null) } - }).catch(function (err) { - logger.error(err); - return callback(err, null); - }); + note.updatetime = moment(_note.lastchangeAt).valueOf() + emitCheck(note) + return callback(null, null) + }) + } else { + return callback(null, null) + } + }, function (err) { + if (err) return logger.error('updater error', err) + }) +}, 1000) + +function updateNote (note, callback) { + models.Note.findOne({ + where: { + id: note.id + } + }).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({ + where: { + id: note.lastchangeuser + } + }).then(function (user) { + if (!user) return callback(null, null) + note.lastchangeuserprofile = models.User.getProfile(user) + return finishUpdateNote(note, _note, callback) + }).catch(function (err) { + logger.error(err) + return callback(err, null) + }) + } else { + return finishUpdateNote(note, _note, callback) + } + } else { + note.lastchangeuserprofile = null + return finishUpdateNote(note, _note, callback) + } + }).catch(function (err) { + logger.error(err) + return callback(err, null) + }) } -function finishUpdateNote(note, _note, callback) { - if (!note || !note.server) return callback(null, null); - var body = note.server.document; - var title = note.title = models.Note.parseNoteTitle(body); - var values = { - title: title, - content: body, - authorship: note.authorship, - lastchangeuserId: note.lastchangeuser, - lastchangeAt: Date.now() - }; - _note.update(values).then(function (_note) { - saverSleep = false; - return callback(null, _note); - }).catch(function (err) { - logger.error(err); - return callback(err, null); - }); + +function finishUpdateNote (note, _note, callback) { + if (!note || !note.server) return callback(null, null) + var body = note.server.document + var title = note.title = models.Note.parseNoteTitle(body) + var values = { + title: title, + content: body, + authorship: note.authorship, + lastchangeuserId: note.lastchangeuser, + lastchangeAt: Date.now() + } + _note.update(values).then(function (_note) { + saverSleep = false + return callback(null, _note) + }).catch(function (err) { + logger.error(err) + return callback(err, null) + }) } -//clean when user not in any rooms or user not in connected list -var cleaner = setInterval(function () { - async.each(Object.keys(users), function (key, callback) { - var socket = realtime.io.sockets.connected[key]; - if ((!socket && users[key]) || - (socket && (!socket.rooms || socket.rooms.length <= 0))) { - if (config.debug) - logger.info("cleaner found redundant user: " + key); - if (!socket) { - socket = { - id: key - }; - } - disconnectSocketQueue.push(socket); - disconnect(socket); + +// clean when user not in any rooms or user not in connected list +setInterval(function () { + async.each(Object.keys(users), function (key, callback) { + var socket = realtime.io.sockets.connected[key] + if ((!socket && users[key]) || + (socket && (!socket.rooms || socket.rooms.length <= 0))) { + if (config.debug) { logger.info('cleaner found redundant user: ' + key) } + if (!socket) { + socket = { + id: key } - return callback(null, null); - }, function (err) { - if (err) return logger.error('cleaner error', err); - }); -}, 60000); -var saverSleep = false; + } + disconnectSocketQueue.push(socket) + disconnect(socket) + } + return callback(null, null) + }, function (err) { + if (err) return logger.error('cleaner error', err) + }) +}, 60000) + +var saverSleep = false // save note revision in interval -var saver = setInterval(function () { - if (saverSleep) return; - models.Revision.saveAllNotesRevision(function (err, notes) { - if (err) return logger.error('revision saver failed: ' + err); - if (notes && notes.length <= 0) { - saverSleep = true; - return; +setInterval(function () { + if (saverSleep) return + models.Revision.saveAllNotesRevision(function (err, notes) { + if (err) return logger.error('revision saver failed: ' + err) + if (notes && notes.length <= 0) { + saverSleep = true + } + }) +}, 60000 * 5) + +function getStatus (callback) { + models.Note.count().then(function (notecount) { + var distinctaddresses = [] + var regaddresses = [] + var distinctregaddresses = [] + Object.keys(users).forEach(function (key) { + var user = users[key] + if (!user) return + let found = false + for (let i = 0; i < distinctaddresses.length; i++) { + if (user.address === distinctaddresses[i]) { + found = true + break } - }); -}, 60000 * 5); - -function getStatus(callback) { - models.Note.count().then(function (notecount) { - var distinctaddresses = []; - var regaddresses = []; - var distinctregaddresses = []; - Object.keys(users).forEach(function (key) { - var user = users[key]; - if (!user) return; - var found = false; - for (var i = 0; i < distinctaddresses.length; i++) { - if (user.address == distinctaddresses[i]) { - found = true; - break; - } - } - if (!found) { - distinctaddresses.push(user.address); - } - if (user.login) { - regaddresses.push(user.address); - var found = false; - for (var i = 0; i < distinctregaddresses.length; i++) { - if (user.address == distinctregaddresses[i]) { - found = true; - break; - } - } - if (!found) { - distinctregaddresses.push(user.address); - } - } - }); - models.User.count().then(function (regcount) { - return callback ? callback({ - onlineNotes: Object.keys(notes).length, - onlineUsers: Object.keys(users).length, - distinctOnlineUsers: distinctaddresses.length, - notesCount: notecount, - registeredUsers: regcount, - onlineRegisteredUsers: regaddresses.length, - distinctOnlineRegisteredUsers: distinctregaddresses.length, - isConnectionBusy: isConnectionBusy, - connectionSocketQueueLength: connectionSocketQueue.length, - isDisconnectBusy: isDisconnectBusy, - disconnectSocketQueueLength: disconnectSocketQueue.length - }) : null; - }).catch(function (err) { - return logger.error('count user failed: ' + err); - }); + } + if (!found) { + distinctaddresses.push(user.address) + } + if (user.login) { + regaddresses.push(user.address) + let found = false + for (let i = 0; i < distinctregaddresses.length; i++) { + if (user.address === distinctregaddresses[i]) { + found = true + break + } + } + if (!found) { + distinctregaddresses.push(user.address) + } + } + }) + models.User.count().then(function (regcount) { + return callback ? callback({ + onlineNotes: Object.keys(notes).length, + onlineUsers: Object.keys(users).length, + distinctOnlineUsers: distinctaddresses.length, + notesCount: notecount, + registeredUsers: regcount, + onlineRegisteredUsers: regaddresses.length, + distinctOnlineRegisteredUsers: distinctregaddresses.length, + isConnectionBusy: isConnectionBusy, + connectionSocketQueueLength: connectionSocketQueue.length, + isDisconnectBusy: isDisconnectBusy, + disconnectSocketQueueLength: disconnectSocketQueue.length + }) : null }).catch(function (err) { - return logger.error('count note failed: ' + err); - }); + return logger.error('count user failed: ' + err) + }) + }).catch(function (err) { + return logger.error('count note failed: ' + err) + }) } -function isReady() { - return realtime.io - && Object.keys(notes).length == 0 && Object.keys(users).length == 0 - && connectionSocketQueue.length == 0 && !isConnectionBusy - && disconnectSocketQueue.length == 0 && !isDisconnectBusy; +function isReady () { + return realtime.io && + Object.keys(notes).length === 0 && Object.keys(users).length === 0 && + connectionSocketQueue.length === 0 && !isConnectionBusy && + disconnectSocketQueue.length === 0 && !isDisconnectBusy } -function extractNoteIdFromSocket(socket) { - if (!socket || !socket.handshake || !socket.handshake.headers) { - return false; - } - var referer = socket.handshake.headers.referer; - if (!referer) { - return false; - } - var hostUrl = url.parse(referer); - var noteId = config.urlpath ? hostUrl.pathname.slice(config.urlpath.length + 1, hostUrl.pathname.length).split('/')[1] : hostUrl.pathname.split('/')[1]; - return noteId; +function extractNoteIdFromSocket (socket) { + if (!socket || !socket.handshake || !socket.handshake.headers) { + return false + } + var referer = socket.handshake.headers.referer + if (!referer) { + return false + } + var hostUrl = url.parse(referer) + var noteId = config.urlpath ? hostUrl.pathname.slice(config.urlpath.length + 1, hostUrl.pathname.length).split('/')[1] : hostUrl.pathname.split('/')[1] + return noteId } -function parseNoteIdFromSocket(socket, callback) { - var noteId = extractNoteIdFromSocket(socket); - if (!noteId) { - return callback(null, null); - } - models.Note.parseNoteId(noteId, function (err, id) { - if (err || !id) return callback(err, id); - return callback(null, id); - }); +function parseNoteIdFromSocket (socket, callback) { + var noteId = extractNoteIdFromSocket(socket) + if (!noteId) { + return callback(null, null) + } + models.Note.parseNoteId(noteId, function (err, id) { + if (err || !id) return callback(err, id) + return callback(null, id) + }) } -function emitOnlineUsers(socket) { - var noteId = socket.noteId; - if (!noteId || !notes[noteId]) return; - var users = []; - Object.keys(notes[noteId].users).forEach(function (key) { - var user = notes[noteId].users[key]; - if (user) - users.push(buildUserOutData(user)); - }); - var out = { - users: users - }; - realtime.io.to(noteId).emit('online users', out); +function emitOnlineUsers (socket) { + var noteId = socket.noteId + if (!noteId || !notes[noteId]) return + var users = [] + Object.keys(notes[noteId].users).forEach(function (key) { + var user = notes[noteId].users[key] + if (user) { users.push(buildUserOutData(user)) } + }) + var out = { + users: users + } + realtime.io.to(noteId).emit('online users', out) } -function emitUserStatus(socket) { - var noteId = socket.noteId; - var user = users[socket.id]; - if (!noteId || !notes[noteId] || !user) return; - var out = buildUserOutData(user); - socket.broadcast.to(noteId).emit('user status', out); +function emitUserStatus (socket) { + var noteId = socket.noteId + var user = users[socket.id] + if (!noteId || !notes[noteId] || !user) return + var out = buildUserOutData(user) + socket.broadcast.to(noteId).emit('user status', out) } -function emitRefresh(socket) { - var noteId = socket.noteId; - if (!noteId || !notes[noteId]) return; - var note = notes[noteId]; - var out = { - title: note.title, - docmaxlength: config.documentmaxlength, - owner: note.owner, - ownerprofile: note.ownerprofile, - lastchangeuser: note.lastchangeuser, - lastchangeuserprofile: note.lastchangeuserprofile, - authors: note.authors, - authorship: note.authorship, - permission: note.permission, - createtime: note.createtime, - updatetime: note.updatetime - }; - socket.emit('refresh', out); +function emitRefresh (socket) { + var noteId = socket.noteId + if (!noteId || !notes[noteId]) return + var note = notes[noteId] + var out = { + title: note.title, + docmaxlength: config.documentmaxlength, + owner: note.owner, + ownerprofile: note.ownerprofile, + lastchangeuser: note.lastchangeuser, + lastchangeuserprofile: note.lastchangeuserprofile, + authors: note.authors, + authorship: note.authorship, + permission: note.permission, + createtime: note.createtime, + updatetime: note.updatetime + } + socket.emit('refresh', out) } -function isDuplicatedInSocketQueue(queue, socket) { - for (var i = 0; i < queue.length; i++) { - if (queue[i] && queue[i].id == socket.id) { - return true; - } +function isDuplicatedInSocketQueue (queue, socket) { + for (var i = 0; i < queue.length; i++) { + if (queue[i] && queue[i].id === socket.id) { + return true } - return false; + } + return false } -function clearSocketQueue(queue, socket) { - for (var i = 0; i < queue.length; i++) { - if (!queue[i] || queue[i].id == socket.id) { - queue.splice(i, 1); - i--; - } +function clearSocketQueue (queue, socket) { + for (var i = 0; i < queue.length; i++) { + if (!queue[i] || queue[i].id === socket.id) { + queue.splice(i, 1) + i-- } + } } -function connectNextSocket() { - setTimeout(function () { - isConnectionBusy = false; - if (connectionSocketQueue.length > 0) { - startConnection(connectionSocketQueue[0]); - } - }, 1); +function connectNextSocket () { + setTimeout(function () { + isConnectionBusy = false + if (connectionSocketQueue.length > 0) { + startConnection(connectionSocketQueue[0]) + } + }, 1) } -function interruptConnection(socket, note, user) { - if (note) delete note; - if (user) delete user; - if (socket) - clearSocketQueue(connectionSocketQueue, socket); - else - connectionSocketQueue.shift(); - connectNextSocket(); +function interruptConnection (socket, noteId, socketId) { + if (notes[noteId]) delete notes[noteId] + if (users[socketId]) delete users[socketId] + if (socket) { clearSocketQueue(connectionSocketQueue, socket) } else { connectionSocketQueue.shift() } + connectNextSocket() } -function checkViewPermission(req, note) { - if (note.permission == 'private') { - if (req.user && req.user.logged_in && req.user.id == note.owner) - return true; - else - return false; - } else if (note.permission == 'limited' || note.permission == 'protected') { - if(req.user && req.user.logged_in) - return true; - else - return false; - } else { - return true; - } +function checkViewPermission (req, note) { + if (note.permission === 'private') { + if (req.user && req.user.logged_in && req.user.id === note.owner) { return true } else { return false } + } else if (note.permission === 'limited' || note.permission === 'protected') { + if (req.user && req.user.logged_in) { return true } else { return false } + } else { + return true + } } -var isConnectionBusy = false; -var connectionSocketQueue = []; -var isDisconnectBusy = false; -var disconnectSocketQueue = []; - -function finishConnection(socket, note, user) { - // if no valid info provided will drop the client - if (!socket || !note || !user) { - return interruptConnection(socket, note, user); - } - // check view permission - if (!checkViewPermission(socket.request, note)) { - interruptConnection(socket, note, user); - return failConnection(403, 'connection forbidden', socket); - } - // update user color to author color - if (note.authors[user.userid]) { - user.color = users[socket.id].color = note.authors[user.userid].color; - } - note.users[socket.id] = user; - note.socks.push(socket); - note.server.addClient(socket); - note.server.setName(socket, user.name); - note.server.setColor(socket, user.color); - - // update user note history - updateHistory(user.userid, note); - - emitOnlineUsers(socket); - emitRefresh(socket); - - //clear finished socket in queue - clearSocketQueue(connectionSocketQueue, socket); - //seek for next socket - connectNextSocket(); - - if (config.debug) { - var noteId = socket.noteId; - logger.info('SERVER connected a client to [' + noteId + ']:'); - logger.info(JSON.stringify(user)); - //logger.info(notes); - getStatus(function (data) { - logger.info(JSON.stringify(data)); - }); - } +var isConnectionBusy = false +var connectionSocketQueue = [] +var isDisconnectBusy = false +var disconnectSocketQueue = [] + +function finishConnection (socket, noteId, socketId) { + // if no valid info provided will drop the client + if (!socket || !notes[noteId] || !users[socketId]) { + return interruptConnection(socket, noteId, socketId) + } + // check view permission + if (!checkViewPermission(socket.request, notes[noteId])) { + interruptConnection(socket, noteId, socketId) + return failConnection(403, 'connection forbidden', socket) + } + let note = notes[noteId] + let user = users[socketId] + // update user color to author color + if (note.authors[user.userid]) { + user.color = users[socket.id].color = note.authors[user.userid].color + } + note.users[socket.id] = user + note.socks.push(socket) + note.server.addClient(socket) + note.server.setName(socket, user.name) + note.server.setColor(socket, user.color) + + // update user note history + updateHistory(user.userid, note) + + emitOnlineUsers(socket) + emitRefresh(socket) + + // clear finished socket in queue + clearSocketQueue(connectionSocketQueue, socket) + // seek for next socket + connectNextSocket() + + if (config.debug) { + let noteId = socket.noteId + logger.info('SERVER connected a client to [' + noteId + ']:') + logger.info(JSON.stringify(user)) + // logger.info(notes); + getStatus(function (data) { + logger.info(JSON.stringify(data)) + }) + } } -function startConnection(socket) { - if (isConnectionBusy) return; - isConnectionBusy = true; +function startConnection (socket) { + if (isConnectionBusy) return + isConnectionBusy = true + + var noteId = socket.noteId + if (!noteId) { + return failConnection(404, 'note id not found', socket) + } + + if (!notes[noteId]) { + var include = [{ + model: models.User, + as: 'owner' + }, { + model: models.User, + as: 'lastchangeuser' + }, { + model: models.Author, + as: 'authors', + include: [{ + model: models.User, + as: 'user' + }] + }] - var noteId = socket.noteId; - if (!noteId) { - return failConnection(404, 'note id not found', socket); - } + models.Note.findOne({ + where: { + id: noteId + }, + include: include + }).then(function (note) { + if (!note) { + return failConnection(404, 'note not found', socket) + } + var owner = note.ownerId + var ownerprofile = note.owner ? models.User.getProfile(note.owner) : null + + var lastchangeuser = note.lastchangeuserId + var lastchangeuserprofile = note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null + + var body = note.content + var createtime = note.createdAt + var updatetime = note.lastchangeAt + var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit, operationCallback) + + var authors = {} + for (var i = 0; i < note.authors.length; i++) { + var author = note.authors[i] + var profile = models.User.getProfile(author.user) + authors[author.userId] = { + userid: author.userId, + color: author.color, + photo: profile.photo, + name: profile.name + } + } - if (!notes[noteId]) { - var include = [{ - model: models.User, - as: "owner" - }, { - model: models.User, - as: "lastchangeuser" - }, { - model: models.Author, - as: "authors", - include: [{ - model: models.User, - as: "user" - }] - }]; - - models.Note.findOne({ - where: { - id: noteId - }, - include: include - }).then(function (note) { - if (!note) { - return failConnection(404, 'note not found', socket); - } - var owner = note.ownerId; - var ownerprofile = note.owner ? models.User.getProfile(note.owner) : null; - - var lastchangeuser = note.lastchangeuserId; - var lastchangeuserprofile = note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null; - - var body = note.content; - var createtime = note.createdAt; - var updatetime = note.lastchangeAt; - var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit, operationCallback); - - var authors = {}; - for (var i = 0; i < note.authors.length; i++) { - var author = note.authors[i]; - var profile = models.User.getProfile(author.user); - authors[author.userId] = { - userid: author.userId, - color: author.color, - photo: profile.photo, - name: profile.name - }; - } + notes[noteId] = { + id: noteId, + alias: note.alias, + title: note.title, + owner: owner, + ownerprofile: ownerprofile, + permission: note.permission, + lastchangeuser: lastchangeuser, + lastchangeuserprofile: lastchangeuserprofile, + socks: [], + users: {}, + tempUsers: {}, + createtime: moment(createtime).valueOf(), + updatetime: moment(updatetime).valueOf(), + server: server, + authors: authors, + authorship: note.authorship + } - notes[noteId] = { - id: noteId, - alias: note.alias, - title: note.title, - owner: owner, - ownerprofile: ownerprofile, - permission: note.permission, - lastchangeuser: lastchangeuser, - lastchangeuserprofile: lastchangeuserprofile, - socks: [], - users: {}, - tempUsers: {}, - createtime: moment(createtime).valueOf(), - updatetime: moment(updatetime).valueOf(), - server: server, - authors: authors, - authorship: note.authorship - }; - - return finishConnection(socket, notes[noteId], users[socket.id]); - }).catch(function (err) { - return failConnection(500, err, socket); - }); - } else { - return finishConnection(socket, notes[noteId], users[socket.id]); - } + return finishConnection(socket, noteId, socket.id) + }).catch(function (err) { + return failConnection(500, err, socket) + }) + } else { + return finishConnection(socket, noteId, socket.id) + } } -function failConnection(code, err, socket) { - logger.error(err); - // clear error socket in queue - clearSocketQueue(connectionSocketQueue, socket); - connectNextSocket(); - // emit error info - socket.emit('info', { - code: code - }); - return socket.disconnect(true); +function failConnection (code, err, socket) { + logger.error(err) + // clear error socket in queue + clearSocketQueue(connectionSocketQueue, socket) + connectNextSocket() + // emit error info + socket.emit('info', { + code: code + }) + return socket.disconnect(true) } -function disconnect(socket) { - if (isDisconnectBusy) return; - isDisconnectBusy = true; - - if (config.debug) { - logger.info("SERVER disconnected a client"); - logger.info(JSON.stringify(users[socket.id])); +function disconnect (socket) { + if (isDisconnectBusy) return + isDisconnectBusy = true + + if (config.debug) { + logger.info('SERVER disconnected a client') + logger.info(JSON.stringify(users[socket.id])) + } + + if (users[socket.id]) { + delete users[socket.id] + } + var noteId = socket.noteId + var note = notes[noteId] + if (note) { + // delete user in users + if (note.users[socket.id]) { + delete note.users[socket.id] } - - if (users[socket.id]) { - delete users[socket.id]; - } - var noteId = socket.noteId; - var note = notes[noteId]; - if (note) { - // delete user in users - if (note.users[socket.id]) { - delete note.users[socket.id]; - } - // remove sockets in the note socks - do { - var index = note.socks.indexOf(socket); - if (index != -1) { - note.socks.splice(index, 1); - } - } while (index != -1); - // remove note in notes if no user inside - if (Object.keys(note.users).length <= 0) { - if (note.server.isDirty) { - updateNote(note, function (err, _note) { - if (err) return logger.error('disconnect note failed: ' + err); - // clear server before delete to avoid memory leaks - note.server.document = ""; - note.server.operations = []; - delete note.server; - delete notes[noteId]; - if (config.debug) { - //logger.info(notes); - getStatus(function (data) { - logger.info(JSON.stringify(data)); - }); - } - }); - } else { - delete note.server; - delete notes[noteId]; - } - } - } - emitOnlineUsers(socket); - - //clear finished socket in queue - clearSocketQueue(disconnectSocketQueue, socket); - //seek for next socket - isDisconnectBusy = false; - if (disconnectSocketQueue.length > 0) - disconnect(disconnectSocketQueue[0]); - - if (config.debug) { - //logger.info(notes); - getStatus(function (data) { - logger.info(JSON.stringify(data)); - }); + // remove sockets in the note socks + do { + var index = note.socks.indexOf(socket) + if (index !== -1) { + note.socks.splice(index, 1) + } + } while (index !== -1) + // remove note in notes if no user inside + if (Object.keys(note.users).length <= 0) { + if (note.server.isDirty) { + updateNote(note, function (err, _note) { + if (err) return logger.error('disconnect note failed: ' + err) + // clear server before delete to avoid memory leaks + note.server.document = '' + note.server.operations = [] + delete note.server + delete notes[noteId] + if (config.debug) { + // logger.info(notes); + getStatus(function (data) { + logger.info(JSON.stringify(data)) + }) + } + }) + } else { + delete note.server + delete notes[noteId] + } } + } + emitOnlineUsers(socket) + + // clear finished socket in queue + clearSocketQueue(disconnectSocketQueue, socket) + // seek for next socket + isDisconnectBusy = false + if (disconnectSocketQueue.length > 0) { disconnect(disconnectSocketQueue[0]) } + + if (config.debug) { + // logger.info(notes); + getStatus(function (data) { + logger.info(JSON.stringify(data)) + }) + } } -function buildUserOutData(user) { - var out = { - id: user.id, - login: user.login, - userid: user.userid, - photo: user.photo, - color: user.color, - cursor: user.cursor, - name: user.name, - idle: user.idle, - type: user.type - }; - return out; +function buildUserOutData (user) { + var out = { + id: user.id, + login: user.login, + userid: user.userid, + photo: user.photo, + color: user.color, + cursor: user.cursor, + name: user.name, + idle: user.idle, + type: user.type + } + return out } -function updateUserData(socket, user) { - //retrieve user data from passport - if (socket.request.user && socket.request.user.logged_in) { - var profile = models.User.getProfile(socket.request.user); - user.photo = profile.photo; - user.name = profile.name; - user.userid = socket.request.user.id; - user.login = true; - } else { - user.userid = null; - user.name = 'Guest ' + chance.last(); - user.login = false; - } +function updateUserData (socket, user) { + // retrieve user data from passport + if (socket.request.user && socket.request.user.logged_in) { + var profile = models.User.getProfile(socket.request.user) + user.photo = profile.photo + user.name = profile.name + user.userid = socket.request.user.id + user.login = true + } else { + user.userid = null + user.name = 'Guest ' + chance.last() + user.login = false + } } -function ifMayEdit(socket, callback) { - var noteId = socket.noteId; - if (!noteId || !notes[noteId]) return; - var note = notes[noteId]; - var mayEdit = true; - switch (note.permission) { - case "freely": - //not blocking anyone - break; - case "editable": case "limited": - //only login user can change - if (!socket.request.user || !socket.request.user.logged_in) - mayEdit = false; - break; - case "locked": case "private": case "protected": - //only owner can change - if (!note.owner || note.owner != socket.request.user.id) - mayEdit = false; - break; - } - //if user may edit and this is a text operation - if (socket.origin == 'operation' && mayEdit) { - //save for the last change user id - if (socket.request.user && socket.request.user.logged_in) { - note.lastchangeuser = socket.request.user.id; - } else { - note.lastchangeuser = null; - } +function ifMayEdit (socket, callback) { + var noteId = socket.noteId + if (!noteId || !notes[noteId]) return + var note = notes[noteId] + var mayEdit = true + switch (note.permission) { + case 'freely': + // not blocking anyone + break + case 'editable': case 'limited': + // only login user can change + if (!socket.request.user || !socket.request.user.logged_in) { mayEdit = false } + break + case 'locked': case 'private': case 'protected': + // only owner can change + if (!note.owner || note.owner !== socket.request.user.id) { mayEdit = false } + break + } + // if user may edit and this is a text operation + if (socket.origin === 'operation' && mayEdit) { + // save for the last change user id + if (socket.request.user && socket.request.user.logged_in) { + note.lastchangeuser = socket.request.user.id + } else { + note.lastchangeuser = null } - return callback(mayEdit); + } + return callback(mayEdit) } -function operationCallback(socket, operation) { - var noteId = socket.noteId; - if (!noteId || !notes[noteId]) return; - var note = notes[noteId]; - var userId = null; - // save authors - if (socket.request.user && socket.request.user.logged_in) { - var user = users[socket.id]; - if (!user) return; - userId = socket.request.user.id; - if (!note.authors[userId]) { - models.Author.findOrCreate({ - where: { - noteId: noteId, - userId: userId - }, - defaults: { - noteId: noteId, - userId: userId, - color: user.color - } - }).spread(function (author, created) { - if (author) { - note.authors[author.userId] = { - userid: author.userId, - color: author.color, - photo: user.photo, - name: user.name - }; - } - }).catch(function (err) { - return logger.error('operation callback failed: ' + err); - }); +function operationCallback (socket, operation) { + var noteId = socket.noteId + if (!noteId || !notes[noteId]) return + var note = notes[noteId] + var userId = null + // save authors + if (socket.request.user && socket.request.user.logged_in) { + var user = users[socket.id] + if (!user) return + userId = socket.request.user.id + if (!note.authors[userId]) { + models.Author.findOrCreate({ + where: { + noteId: noteId, + userId: userId + }, + defaults: { + noteId: noteId, + userId: userId, + color: user.color + } + }).spread(function (author, created) { + if (author) { + note.authors[author.userId] = { + userid: author.userId, + color: author.color, + photo: user.photo, + name: user.name + } } - note.tempUsers[userId] = Date.now(); + }).catch(function (err) { + return logger.error('operation callback failed: ' + err) + }) } - // save authorship - use timer here because it's an O(n) complexity algorithm - setImmediate(function () { - note.authorship = models.Note.updateAuthorshipByOperation(operation, userId, note.authorship); - }); + note.tempUsers[userId] = Date.now() + } + // 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 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) { - if (config.maintenance) return; - parseNoteIdFromSocket(socket, function (err, noteId) { - if (err) { - return failConnection(500, err, socket); - } - if (!noteId) { - return failConnection(404, 'note id not found', socket); - } +function connection (socket) { + if (config.maintenance) return + parseNoteIdFromSocket(socket, function (err, noteId) { + if (err) { + return failConnection(500, err, socket) + } + if (!noteId) { + return failConnection(404, 'note id not found', socket) + } - if (isDuplicatedInSocketQueue(socket, connectionSocketQueue)) return; - - // store noteId in this socket session - socket.noteId = noteId; - - //initialize user data - //random color - var color = randomcolor(); - //make sure color not duplicated or reach max random count - if (notes[noteId]) { - var randomcount = 0; - var maxrandomcount = 10; - var found = false; - do { - Object.keys(notes[noteId].users).forEach(function (user) { - if (user.color == color) { - found = true; - return; - } - }); - if (found) { - color = randomcolor(); - randomcount++; - } - } while (found && randomcount < maxrandomcount); + if (isDuplicatedInSocketQueue(socket, connectionSocketQueue)) return + + // store noteId in this socket session + socket.noteId = noteId + + // initialize user data + // random color + var color = randomcolor() + // make sure color not duplicated or reach max random count + if (notes[noteId]) { + var randomcount = 0 + var maxrandomcount = 10 + var found = false + do { + Object.keys(notes[noteId].users).forEach(function (user) { + if (user.color === color) { + found = true + } + }) + if (found) { + color = randomcolor() + randomcount++ } - //create user data - users[socket.id] = { - id: socket.id, - address: socket.handshake.headers['x-forwarded-for'] || socket.handshake.address, - 'user-agent': socket.handshake.headers['user-agent'], - color: color, - cursor: null, - login: false, - userid: null, - name: null, - idle: false, - type: null - }; - updateUserData(socket, users[socket.id]); - - //start connection - connectionSocketQueue.push(socket); - startConnection(socket); - }); - - //received client refresh request - socket.on('refresh', function () { - emitRefresh(socket); - }); - - //received user status - socket.on('user status', function (data) { - var noteId = socket.noteId; - var user = users[socket.id]; - if (!noteId || !notes[noteId] || !user) return; - if (config.debug) - logger.info('SERVER received [' + noteId + '] user status from [' + socket.id + ']: ' + JSON.stringify(data)); - if (data) { - user.idle = data.idle; - user.type = data.type; - } - emitUserStatus(socket); - }); - - //received note permission change request - socket.on('permission', function (permission) { - //need login to do more actions - if (socket.request.user && socket.request.user.logged_in) { - var noteId = socket.noteId; - if (!noteId || !notes[noteId]) return; - var note = notes[noteId]; - //Only owner can change permission - if (note.owner && note.owner == socket.request.user.id) { - if (permission == 'freely' && !config.allowanonymous) return; - note.permission = permission; - models.Note.update({ - permission: permission - }, { - where: { - id: noteId - } - }).then(function (count) { - if (!count) { - return; - } - var out = { - permission: permission - }; - realtime.io.to(note.id).emit('permission', out); - for (var i = 0, l = note.socks.length; i < l; i++) { - var sock = note.socks[i]; - if (typeof sock !== 'undefined' && sock) { - // check view permission - if (!checkViewPermission(sock.request, note)) { - sock.emit('info', { - code: 403 - }); - setTimeout(function () { - sock.disconnect(true); - }, 0); - } - } - } - }).catch(function (err) { - return logger.error('update note permission failed: ' + err); - }); + } while (found && randomcount < maxrandomcount) + } + // create user data + users[socket.id] = { + id: socket.id, + address: socket.handshake.headers['x-forwarded-for'] || socket.handshake.address, + 'user-agent': socket.handshake.headers['user-agent'], + color: color, + cursor: null, + login: false, + userid: null, + name: null, + idle: false, + type: null + } + updateUserData(socket, users[socket.id]) + + // start connection + connectionSocketQueue.push(socket) + startConnection(socket) + }) + + // received client refresh request + socket.on('refresh', function () { + emitRefresh(socket) + }) + + // received user status + socket.on('user status', function (data) { + var noteId = socket.noteId + var user = users[socket.id] + if (!noteId || !notes[noteId] || !user) return + if (config.debug) { logger.info('SERVER received [' + noteId + '] user status from [' + socket.id + ']: ' + JSON.stringify(data)) } + if (data) { + user.idle = data.idle + user.type = data.type + } + emitUserStatus(socket) + }) + + // received note permission change request + socket.on('permission', function (permission) { + // need login to do more actions + if (socket.request.user && socket.request.user.logged_in) { + var noteId = socket.noteId + if (!noteId || !notes[noteId]) return + var note = notes[noteId] + // Only owner can change permission + if (note.owner && note.owner === socket.request.user.id) { + if (permission === 'freely' && !config.allowanonymous) return + note.permission = permission + models.Note.update({ + permission: permission + }, { + where: { + id: noteId + } + }).then(function (count) { + if (!count) { + return + } + var out = { + permission: permission + } + realtime.io.to(note.id).emit('permission', out) + for (var i = 0, l = note.socks.length; i < l; i++) { + var sock = note.socks[i] + if (typeof sock !== 'undefined' && sock) { + // check view permission + if (!checkViewPermission(sock.request, note)) { + sock.emit('info', { + code: 403 + }) + setTimeout(function () { + sock.disconnect(true) + }, 0) + } } - } - }); - - // delete a note - socket.on('delete', function () { - //need login to do more actions - if (socket.request.user && socket.request.user.logged_in) { - var noteId = socket.noteId; - if (!noteId || !notes[noteId]) return; - var note = notes[noteId]; - //Only owner can delete note - if (note.owner && note.owner == socket.request.user.id) { - models.Note.destroy({ - where: { - id: noteId - } - }).then(function (count) { - if (!count) return; - for (var i = 0, l = note.socks.length; i < l; i++) { - var sock = note.socks[i]; - if (typeof sock !== 'undefined' && sock) { - sock.emit('delete'); - setTimeout(function () { - sock.disconnect(true); - }, 0); - } - } - }).catch(function (err) { - return logger.error('delete note failed: ' + err); - }); + } + }).catch(function (err) { + return logger.error('update note permission failed: ' + err) + }) + } + } + }) + + // delete a note + socket.on('delete', function () { + // need login to do more actions + if (socket.request.user && socket.request.user.logged_in) { + var noteId = socket.noteId + if (!noteId || !notes[noteId]) return + var note = notes[noteId] + // Only owner can delete note + if (note.owner && note.owner === socket.request.user.id) { + models.Note.destroy({ + where: { + id: noteId + } + }).then(function (count) { + if (!count) return + for (var i = 0, l = note.socks.length; i < l; i++) { + var sock = note.socks[i] + if (typeof sock !== 'undefined' && sock) { + sock.emit('delete') + setTimeout(function () { + sock.disconnect(true) + }, 0) } - } - }); - - //reveiced when user logout or changed - socket.on('user changed', function () { - logger.info('user changed'); - var noteId = socket.noteId; - if (!noteId || !notes[noteId]) return; - var user = notes[noteId].users[socket.id]; - if (!user) return; - updateUserData(socket, user); - emitOnlineUsers(socket); - }); - - //received sync of online users request - socket.on('online users', function () { - var noteId = socket.noteId; - if (!noteId || !notes[noteId]) return; - var users = []; - Object.keys(notes[noteId].users).forEach(function (key) { - var user = notes[noteId].users[key]; - if (user) - users.push(buildUserOutData(user)); - }); - var out = { - users: users - }; - socket.emit('online users', out); - }); - - //check version - socket.on('version', function () { - socket.emit('version', { - version: config.version, - minimumCompatibleVersion: config.minimumCompatibleVersion - }); - }); - - //received cursor focus - socket.on('cursor focus', function (data) { - var noteId = socket.noteId; - var user = users[socket.id]; - if (!noteId || !notes[noteId] || !user) return; - user.cursor = data; - var out = buildUserOutData(user); - socket.broadcast.to(noteId).emit('cursor focus', out); - }); - - //received cursor activity - socket.on('cursor activity', function (data) { - var noteId = socket.noteId; - var user = users[socket.id]; - if (!noteId || !notes[noteId] || !user) return; - user.cursor = data; - var out = buildUserOutData(user); - socket.broadcast.to(noteId).emit('cursor activity', out); - }); - - //received cursor blur - socket.on('cursor blur', function () { - var noteId = socket.noteId; - var user = users[socket.id]; - if (!noteId || !notes[noteId] || !user) return; - user.cursor = null; - var out = { - id: socket.id - }; - socket.broadcast.to(noteId).emit('cursor blur', out); - }); - - //when a new client disconnect - socket.on('disconnect', function () { - if (isDuplicatedInSocketQueue(socket, disconnectSocketQueue)) return; - disconnectSocketQueue.push(socket); - disconnect(socket); - }); + } + }).catch(function (err) { + return logger.error('delete note failed: ' + err) + }) + } + } + }) + + // reveiced when user logout or changed + socket.on('user changed', function () { + logger.info('user changed') + var noteId = socket.noteId + if (!noteId || !notes[noteId]) return + var user = notes[noteId].users[socket.id] + if (!user) return + updateUserData(socket, user) + emitOnlineUsers(socket) + }) + + // received sync of online users request + socket.on('online users', function () { + var noteId = socket.noteId + if (!noteId || !notes[noteId]) return + var users = [] + Object.keys(notes[noteId].users).forEach(function (key) { + var user = notes[noteId].users[key] + if (user) { users.push(buildUserOutData(user)) } + }) + var out = { + users: users + } + socket.emit('online users', out) + }) + + // check version + socket.on('version', function () { + socket.emit('version', { + version: config.version, + minimumCompatibleVersion: config.minimumCompatibleVersion + }) + }) + + // received cursor focus + socket.on('cursor focus', function (data) { + var noteId = socket.noteId + var user = users[socket.id] + if (!noteId || !notes[noteId] || !user) return + user.cursor = data + var out = buildUserOutData(user) + socket.broadcast.to(noteId).emit('cursor focus', out) + }) + + // received cursor activity + socket.on('cursor activity', function (data) { + var noteId = socket.noteId + var user = users[socket.id] + if (!noteId || !notes[noteId] || !user) return + user.cursor = data + var out = buildUserOutData(user) + socket.broadcast.to(noteId).emit('cursor activity', out) + }) + + // received cursor blur + socket.on('cursor blur', function () { + var noteId = socket.noteId + var user = users[socket.id] + if (!noteId || !notes[noteId] || !user) return + user.cursor = null + var out = { + id: socket.id + } + socket.broadcast.to(noteId).emit('cursor blur', out) + }) + + // when a new client disconnect + socket.on('disconnect', function () { + if (isDuplicatedInSocketQueue(socket, disconnectSocketQueue)) return + disconnectSocketQueue.push(socket) + disconnect(socket) + }) } -module.exports = realtime;
\ No newline at end of file +module.exports = realtime diff --git a/lib/response.js b/lib/response.js index 585d1d54..31fa18b2 100755 --- a/lib/response.js +++ b/lib/response.js @@ -1,609 +1,601 @@ -//response -//external modules -var fs = require('fs'); -var path = require('path'); -var markdownpdf = require("markdown-pdf"); -var LZString = require('lz-string'); -var S = require('string'); -var shortId = require('shortid'); -var querystring = require('querystring'); -var request = require('request'); -var moment = require('moment'); +// response +// external modules +var fs = require('fs') +var markdownpdf = require('markdown-pdf') +var LZString = require('lz-string') +var shortId = require('shortid') +var querystring = require('querystring') +var request = require('request') +var moment = require('moment') -//core -var config = require("./config.js"); -var logger = require("./logger.js"); -var models = require("./models"); +// core +var config = require('./config.js') +var logger = require('./logger.js') +var models = require('./models') -//public +// public var response = { - errorForbidden: function (res) { - responseError(res, "403", "Forbidden", "oh no."); - }, - errorNotFound: function (res) { - responseError(res, "404", "Not Found", "oops."); - }, - errorBadRequest: function (res) { - responseError(res, "400", "Bad Request", "something not right."); - }, - errorInternalError: function (res) { - responseError(res, "500", "Internal Error", "wtf."); - }, - errorServiceUnavailable: function (res) { - res.status(503).send("I'm busy right now, try again later."); - }, - newNote: newNote, - showNote: showNote, - showPublishNote: showPublishNote, - showPublishSlide: showPublishSlide, - showIndex: showIndex, - noteActions: noteActions, - publishNoteActions: publishNoteActions, - publishSlideActions: publishSlideActions, - githubActions: githubActions, - gitlabActions: gitlabActions -}; + errorForbidden: function (res) { + responseError(res, '403', 'Forbidden', 'oh no.') + }, + errorNotFound: function (res) { + responseError(res, '404', 'Not Found', 'oops.') + }, + errorBadRequest: function (res) { + responseError(res, '400', 'Bad Request', 'something not right.') + }, + errorInternalError: function (res) { + responseError(res, '500', 'Internal Error', 'wtf.') + }, + errorServiceUnavailable: function (res) { + res.status(503).send("I'm busy right now, try again later.") + }, + newNote: newNote, + showNote: showNote, + showPublishNote: showPublishNote, + showPublishSlide: showPublishSlide, + showIndex: showIndex, + noteActions: noteActions, + publishNoteActions: publishNoteActions, + publishSlideActions: publishSlideActions, + githubActions: githubActions, + gitlabActions: gitlabActions +} -function responseError(res, code, detail, msg) { - res.status(code).render(config.errorpath, { - url: config.serverurl, - title: code + ' ' + detail + ' ' + msg, - code: code, - detail: detail, - msg: msg, - useCDN: config.usecdn - }); +function responseError (res, code, detail, msg) { + res.status(code).render(config.errorpath, { + url: config.serverurl, + title: code + ' ' + detail + ' ' + msg, + code: code, + detail: detail, + msg: msg, + useCDN: config.usecdn + }) } -function showIndex(req, res, next) { - res.render(config.indexpath, { - url: config.serverurl, - useCDN: config.usecdn, - allowAnonymous: config.allowanonymous, - facebook: config.facebook, - twitter: config.twitter, - github: config.github, - gitlab: config.gitlab, - dropbox: config.dropbox, - google: config.google, - ldap: config.ldap, - email: config.email, - allowemailregister: config.allowemailregister, - signin: req.isAuthenticated(), - infoMessage: req.flash('info'), - errorMessage: req.flash('error') - }); +function showIndex (req, res, next) { + res.render(config.indexpath, { + url: config.serverurl, + useCDN: config.usecdn, + allowAnonymous: config.allowanonymous, + facebook: config.facebook, + twitter: config.twitter, + github: config.github, + gitlab: config.gitlab, + dropbox: config.dropbox, + google: config.google, + ldap: config.ldap, + email: config.email, + allowemailregister: config.allowemailregister, + signin: req.isAuthenticated(), + infoMessage: req.flash('info'), + errorMessage: req.flash('error') + }) } -function responseHackMD(res, note) { - var body = note.content; - var extracted = models.Note.extractMeta(body); - var meta = models.Note.parseMeta(extracted.meta); - var title = models.Note.decodeTitle(note.title); - title = models.Note.generateWebTitle(meta.title || title); - res.set({ - 'Cache-Control': 'private', // only cache by client - 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling - }); - res.render(config.hackmdpath, { - url: config.serverurl, - title: title, - useCDN: config.usecdn, - allowAnonymous: config.allowanonymous, - facebook: config.facebook, - twitter: config.twitter, - github: config.github, - gitlab: config.gitlab, - dropbox: config.dropbox, - google: config.google, - ldap: config.ldap, - email: config.email, - allowemailregister: config.allowemailregister - }); +function responseHackMD (res, note) { + var body = note.content + var extracted = models.Note.extractMeta(body) + var meta = models.Note.parseMeta(extracted.meta) + var title = models.Note.decodeTitle(note.title) + title = models.Note.generateWebTitle(meta.title || title) + res.set({ + 'Cache-Control': 'private', // only cache by client + 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling + }) + res.render(config.hackmdpath, { + url: config.serverurl, + title: title, + useCDN: config.usecdn, + allowAnonymous: config.allowanonymous, + facebook: config.facebook, + twitter: config.twitter, + github: config.github, + gitlab: config.gitlab, + dropbox: config.dropbox, + google: config.google, + ldap: config.ldap, + email: config.email, + allowemailregister: config.allowemailregister + }) } -function newNote(req, res, next) { - var owner = null; - if (req.isAuthenticated()) { - owner = req.user.id; - } else if (!config.allowanonymous) { - return response.errorForbidden(res); - } - models.Note.create({ - ownerId: owner, - alias: req.alias ? req.alias : null - }).then(function (note) { - return res.redirect(config.serverurl + "/" + LZString.compressToBase64(note.id)); - }).catch(function (err) { - logger.error(err); - return response.errorInternalError(res); - }); +function newNote (req, res, next) { + var owner = null + if (req.isAuthenticated()) { + owner = req.user.id + } else if (!config.allowanonymous) { + return response.errorForbidden(res) + } + models.Note.create({ + ownerId: owner, + alias: req.alias ? req.alias : null + }).then(function (note) { + return res.redirect(config.serverurl + '/' + LZString.compressToBase64(note.id)) + }).catch(function (err) { + logger.error(err) + return response.errorInternalError(res) + }) } -function checkViewPermission(req, note) { - if (note.permission == 'private') { - if (!req.isAuthenticated() || note.ownerId != req.user.id) - return false; - else - return true; - } else if (note.permission == 'limited' || note.permission == 'protected') { - if(!req.isAuthenticated()) - return false; - else - return true; - } else { - return true; - } +function checkViewPermission (req, note) { + if (note.permission === 'private') { + if (!req.isAuthenticated() || note.ownerId !== req.user.id) { return false } else { return true } + } else if (note.permission === 'limited' || note.permission === 'protected') { + if (!req.isAuthenticated()) { return false } else { return true } + } else { + return true + } } -function findNote(req, res, callback, include) { - var noteId = req.params.noteId; - var id = req.params.noteId || req.params.shortid; - models.Note.parseNoteId(id, function (err, _id) { - models.Note.findOne({ - where: { - id: _id - }, - include: include || null - }).then(function (note) { - if (!note) { - if (config.allowfreeurl && noteId) { - req.alias = noteId; - return newNote(req, res); - } else { - return response.errorNotFound(res); - } - } - if (!checkViewPermission(req, note)) { - return response.errorForbidden(res); - } else { - return callback(note); - } - }).catch(function (err) { - logger.error(err); - return response.errorInternalError(res); - }); - }); +function findNote (req, res, callback, include) { + var noteId = req.params.noteId + var id = req.params.noteId || req.params.shortid + models.Note.parseNoteId(id, function (err, _id) { + if (err) { + logger.log(err) + } + models.Note.findOne({ + where: { + id: _id + }, + include: include || null + }).then(function (note) { + if (!note) { + if (config.allowfreeurl && noteId) { + req.alias = noteId + return newNote(req, res) + } else { + return response.errorNotFound(res) + } + } + if (!checkViewPermission(req, note)) { + return response.errorForbidden(res) + } else { + return callback(note) + } + }).catch(function (err) { + logger.error(err) + return response.errorInternalError(res) + }) + }) } -function showNote(req, res, next) { - findNote(req, res, function (note) { - // force to use note id - var noteId = req.params.noteId; - var id = LZString.compressToBase64(note.id); - if ((note.alias && noteId != note.alias) || (!note.alias && noteId != id)) - return res.redirect(config.serverurl + "/" + (note.alias || id)); - return responseHackMD(res, note); - }); +function showNote (req, res, next) { + findNote(req, res, function (note) { + // force to use note id + var noteId = req.params.noteId + var id = LZString.compressToBase64(note.id) + if ((note.alias && noteId !== note.alias) || (!note.alias && noteId !== id)) { return res.redirect(config.serverurl + '/' + (note.alias || id)) } + return responseHackMD(res, note) + }) } -function showPublishNote(req, res, next) { - var include = [{ - model: models.User, - as: "owner" - }, { - model: models.User, - as: "lastchangeuser" - }]; - findNote(req, res, function (note) { - // force to use short id - var shortid = req.params.shortid; - if ((note.alias && shortid != note.alias) || (!note.alias && shortid != note.shortid)) - return res.redirect(config.serverurl + "/s/" + (note.alias || note.shortid)); - note.increment('viewcount').then(function (note) { - if (!note) { - return response.errorNotFound(res); - } - var body = note.content; - var extracted = models.Note.extractMeta(body); - markdown = extracted.markdown; - meta = models.Note.parseMeta(extracted.meta); - var createtime = note.createdAt; - var updatetime = note.lastchangeAt; - var title = models.Note.decodeTitle(note.title); - title = models.Note.generateWebTitle(meta.title || title); - var origin = config.serverurl; - var data = { - title: title, - description: meta.description || (markdown ? models.Note.generateDescription(markdown) : null), - viewcount: note.viewcount, - createtime: createtime, - updatetime: updatetime, - url: origin, - body: body, - useCDN: config.usecdn, - owner: note.owner ? note.owner.id : null, - ownerprofile: note.owner ? models.User.getProfile(note.owner) : null, - lastchangeuser: note.lastchangeuser ? note.lastchangeuser.id : null, - lastchangeuserprofile: note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null, - robots: meta.robots || false, //default allow robots - GA: meta.GA, - disqus: meta.disqus - }; - return renderPublish(data, res); - }).catch(function (err) { - logger.error(err); - return response.errorInternalError(res); - }); - }, include); +function showPublishNote (req, res, next) { + var include = [{ + model: models.User, + as: 'owner' + }, { + model: models.User, + as: 'lastchangeuser' + }] + findNote(req, res, function (note) { + // force to use short id + var shortid = req.params.shortid + if ((note.alias && shortid !== note.alias) || (!note.alias && shortid !== note.shortid)) { + return res.redirect(config.serverurl + '/s/' + (note.alias || note.shortid)) + } + note.increment('viewcount').then(function (note) { + if (!note) { + return response.errorNotFound(res) + } + var body = note.content + var extracted = models.Note.extractMeta(body) + var markdown = extracted.markdown + var meta = models.Note.parseMeta(extracted.meta) + var createtime = note.createdAt + var updatetime = note.lastchangeAt + var title = models.Note.decodeTitle(note.title) + title = models.Note.generateWebTitle(meta.title || title) + var origin = config.serverurl + var data = { + title: title, + description: meta.description || (markdown ? models.Note.generateDescription(markdown) : null), + viewcount: note.viewcount, + createtime: createtime, + updatetime: updatetime, + url: origin, + body: body, + useCDN: config.usecdn, + owner: note.owner ? note.owner.id : null, + ownerprofile: note.owner ? models.User.getProfile(note.owner) : null, + lastchangeuser: note.lastchangeuser ? note.lastchangeuser.id : null, + lastchangeuserprofile: note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null, + robots: meta.robots || false, // default allow robots + GA: meta.GA, + disqus: meta.disqus + } + return renderPublish(data, res) + }).catch(function (err) { + logger.error(err) + return response.errorInternalError(res) + }) + }, include) } -function renderPublish(data, res) { - res.set({ - 'Cache-Control': 'private' // only cache by client - }); - res.render(config.prettypath, data); +function renderPublish (data, res) { + res.set({ + 'Cache-Control': 'private' // only cache by client + }) + res.render(config.prettypath, data) } -function actionPublish(req, res, note) { - res.redirect(config.serverurl + "/s/" + (note.alias || note.shortid)); +function actionPublish (req, res, note) { + res.redirect(config.serverurl + '/s/' + (note.alias || note.shortid)) } -function actionSlide(req, res, note) { - res.redirect(config.serverurl + "/p/" + (note.alias || note.shortid)); +function actionSlide (req, res, note) { + res.redirect(config.serverurl + '/p/' + (note.alias || note.shortid)) } -function actionDownload(req, res, note) { - var body = note.content; - var title = models.Note.decodeTitle(note.title); - var filename = title; - filename = encodeURIComponent(filename); - res.set({ - 'Access-Control-Allow-Origin': '*', //allow CORS as API - 'Access-Control-Allow-Headers': 'Range', - 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range', - 'Content-Type': 'text/markdown; charset=UTF-8', - 'Cache-Control': 'private', - 'Content-disposition': 'attachment; filename=' + filename + '.md', - 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling - }); - res.send(body); +function actionDownload (req, res, note) { + var body = note.content + var title = models.Note.decodeTitle(note.title) + var filename = title + filename = encodeURIComponent(filename) + res.set({ + 'Access-Control-Allow-Origin': '*', // allow CORS as API + 'Access-Control-Allow-Headers': 'Range', + 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range', + 'Content-Type': 'text/markdown; charset=UTF-8', + 'Cache-Control': 'private', + 'Content-disposition': 'attachment; filename=' + filename + '.md', + 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling + }) + res.send(body) } -function actionInfo(req, res, note) { - var body = note.content; - var extracted = models.Note.extractMeta(body); - var markdown = extracted.markdown; - var meta = models.Note.parseMeta(extracted.meta); - var createtime = note.createdAt; - var updatetime = note.lastchangeAt; - var title = models.Note.decodeTitle(note.title); - var data = { - title: meta.title || title, - description: meta.description || (markdown ? models.Note.generateDescription(markdown) : null), - viewcount: note.viewcount, - createtime: createtime, - updatetime: updatetime - }; - res.set({ - 'Access-Control-Allow-Origin': '*', //allow CORS as API - 'Access-Control-Allow-Headers': 'Range', - 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range', - 'Cache-Control': 'private', // only cache by client - 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling - }); - res.send(data); +function actionInfo (req, res, note) { + var body = note.content + var extracted = models.Note.extractMeta(body) + var markdown = extracted.markdown + var meta = models.Note.parseMeta(extracted.meta) + var createtime = note.createdAt + var updatetime = note.lastchangeAt + var title = models.Note.decodeTitle(note.title) + var data = { + title: meta.title || title, + description: meta.description || (markdown ? models.Note.generateDescription(markdown) : null), + viewcount: note.viewcount, + createtime: createtime, + updatetime: updatetime + } + res.set({ + 'Access-Control-Allow-Origin': '*', // allow CORS as API + 'Access-Control-Allow-Headers': 'Range', + 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range', + 'Cache-Control': 'private', // only cache by client + 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling + }) + res.send(data) } -function actionPDF(req, res, note) { - var body = note.content; - var extracted = models.Note.extractMeta(body); - var title = models.Note.decodeTitle(note.title); +function actionPDF (req, res, note) { + var body = note.content + var extracted = models.Note.extractMeta(body) + var title = models.Note.decodeTitle(note.title) - if (!fs.existsSync(config.tmppath)) { - fs.mkdirSync(config.tmppath); - } - var path = config.tmppath + '/' + Date.now() + '.pdf'; - markdownpdf().from.string(extracted.markdown).to(path, function () { - var stream = fs.createReadStream(path); - var filename = title; - // Be careful of special characters - filename = encodeURIComponent(filename); - // Ideally this should strip them - res.setHeader('Content-disposition', 'attachment; filename="' + filename + '.pdf"'); - res.setHeader('Cache-Control', 'private'); - res.setHeader('Content-Type', 'application/pdf; charset=UTF-8'); - res.setHeader('X-Robots-Tag', 'noindex, nofollow'); // prevent crawling - stream.pipe(res); - fs.unlink(path); - }); + if (!fs.existsSync(config.tmppath)) { + fs.mkdirSync(config.tmppath) + } + var path = config.tmppath + '/' + Date.now() + '.pdf' + markdownpdf().from.string(extracted.markdown).to(path, function () { + var stream = fs.createReadStream(path) + var filename = title + // Be careful of special characters + filename = encodeURIComponent(filename) + // Ideally this should strip them + res.setHeader('Content-disposition', 'attachment; filename="' + filename + '.pdf"') + res.setHeader('Cache-Control', 'private') + res.setHeader('Content-Type', 'application/pdf; charset=UTF-8') + res.setHeader('X-Robots-Tag', 'noindex, nofollow') // prevent crawling + stream.pipe(res) + fs.unlink(path) + }) } -function actionGist(req, res, note) { - var data = { - client_id: config.github.clientID, - redirect_uri: config.serverurl + '/auth/github/callback/' + LZString.compressToBase64(note.id) + '/gist', - scope: "gist", - state: shortId.generate() - }; - var query = querystring.stringify(data); - res.redirect("https://github.com/login/oauth/authorize?" + query); +function actionGist (req, res, note) { + var data = { + client_id: config.github.clientID, + redirect_uri: config.serverurl + '/auth/github/callback/' + LZString.compressToBase64(note.id) + '/gist', + scope: 'gist', + state: shortId.generate() + } + var query = querystring.stringify(data) + res.redirect('https://github.com/login/oauth/authorize?' + query) } -function actionRevision(req, res, note) { - var actionId = req.params.actionId; - if (actionId) { - var time = moment(parseInt(actionId)); - if (time.isValid()) { - models.Revision.getPatchedNoteRevisionByTime(note, time, function (err, content) { - if (err) { - logger.error(err); - return response.errorInternalError(res); - } - if (!content) { - return response.errorNotFound(res); - } - res.set({ - 'Access-Control-Allow-Origin': '*', //allow CORS as API - 'Access-Control-Allow-Headers': 'Range', - 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range', - 'Cache-Control': 'private', // only cache by client - 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling - }); - res.send(content); - }); - } else { - return response.errorNotFound(res); +function actionRevision (req, res, note) { + var actionId = req.params.actionId + if (actionId) { + var time = moment(parseInt(actionId)) + if (time.isValid()) { + models.Revision.getPatchedNoteRevisionByTime(note, time, function (err, content) { + if (err) { + logger.error(err) + return response.errorInternalError(res) } + if (!content) { + return response.errorNotFound(res) + } + res.set({ + 'Access-Control-Allow-Origin': '*', // allow CORS as API + 'Access-Control-Allow-Headers': 'Range', + 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range', + 'Cache-Control': 'private', // only cache by client + 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling + }) + res.send(content) + }) } else { - models.Revision.getNoteRevisions(note, function (err, data) { - if (err) { - logger.error(err); - return response.errorInternalError(res); - } - var out = { - revision: data - }; - res.set({ - 'Access-Control-Allow-Origin': '*', //allow CORS as API - 'Access-Control-Allow-Headers': 'Range', - 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range', - 'Cache-Control': 'private', // only cache by client - 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling - }); - res.send(out); - }); + return response.errorNotFound(res) } + } else { + models.Revision.getNoteRevisions(note, function (err, data) { + if (err) { + logger.error(err) + return response.errorInternalError(res) + } + var out = { + revision: data + } + res.set({ + 'Access-Control-Allow-Origin': '*', // allow CORS as API + 'Access-Control-Allow-Headers': 'Range', + 'Access-Control-Expose-Headers': 'Cache-Control, Content-Encoding, Content-Range', + 'Cache-Control': 'private', // only cache by client + 'X-Robots-Tag': 'noindex, nofollow' // prevent crawling + }) + res.send(out) + }) + } } -function noteActions(req, res, next) { - var noteId = req.params.noteId; - findNote(req, res, function (note) { - var action = req.params.action; - switch (action) { - case "publish": - case "pretty": //pretty deprecated - actionPublish(req, res, note); - break; - case "slide": - actionSlide(req, res, note); - break; - case "download": - actionDownload(req, res, note); - break; - case "info": - actionInfo(req, res, note); - break; - case "pdf": - actionPDF(req, res, note); - break; - case "gist": - actionGist(req, res, note); - break; - case "revision": - actionRevision(req, res, note); - break; - default: - return res.redirect(config.serverurl + '/' + noteId); - break; - } - }); +function noteActions (req, res, next) { + var noteId = req.params.noteId + findNote(req, res, function (note) { + var action = req.params.action + switch (action) { + case 'publish': + case 'pretty': // pretty deprecated + actionPublish(req, res, note) + break + case 'slide': + actionSlide(req, res, note) + break + case 'download': + actionDownload(req, res, note) + break + case 'info': + actionInfo(req, res, note) + break + case 'pdf': + actionPDF(req, res, note) + break + case 'gist': + actionGist(req, res, note) + break + case 'revision': + actionRevision(req, res, note) + break + default: + return res.redirect(config.serverurl + '/' + noteId) + } + }) } -function publishNoteActions(req, res, next) { - findNote(req, res, function (note) { - var action = req.params.action; - switch (action) { - case "edit": - res.redirect(config.serverurl + '/' + (note.alias ? note.alias : LZString.compressToBase64(note.id))); - break; - default: - res.redirect(config.serverurl + '/s/' + note.shortid); - break; - } - }); +function publishNoteActions (req, res, next) { + findNote(req, res, function (note) { + var action = req.params.action + switch (action) { + case 'edit': + res.redirect(config.serverurl + '/' + (note.alias ? note.alias : LZString.compressToBase64(note.id))) + break + default: + res.redirect(config.serverurl + '/s/' + note.shortid) + break + } + }) } -function publishSlideActions(req, res, next) { - findNote(req, res, function (note) { - var action = req.params.action; - switch (action) { - case "edit": - res.redirect(config.serverurl + '/' + (note.alias ? note.alias : LZString.compressToBase64(note.id))); - break; - default: - res.redirect(config.serverurl + '/p/' + note.shortid); - break; - } - }); +function publishSlideActions (req, res, next) { + findNote(req, res, function (note) { + var action = req.params.action + switch (action) { + case 'edit': + res.redirect(config.serverurl + '/' + (note.alias ? note.alias : LZString.compressToBase64(note.id))) + break + default: + res.redirect(config.serverurl + '/p/' + note.shortid) + break + } + }) } -function githubActions(req, res, next) { - var noteId = req.params.noteId; - findNote(req, res, function (note) { - var action = req.params.action; - switch (action) { - case "gist": - githubActionGist(req, res, note); - break; - default: - res.redirect(config.serverurl + '/' + noteId); - break; - } - }); +function githubActions (req, res, next) { + var noteId = req.params.noteId + findNote(req, res, function (note) { + var action = req.params.action + switch (action) { + case 'gist': + githubActionGist(req, res, note) + break + default: + res.redirect(config.serverurl + '/' + noteId) + break + } + }) } -function githubActionGist(req, res, note) { - var code = req.query.code; - var state = req.query.state; - if (!code || !state) { - return response.errorForbidden(res); - } else { - var data = { - client_id: config.github.clientID, - client_secret: config.github.clientSecret, - code: code, - state: state - } - var auth_url = 'https://github.com/login/oauth/access_token'; - request({ - url: auth_url, - method: "POST", - json: data - }, function (error, httpResponse, body) { - if (!error && httpResponse.statusCode == 200) { - var access_token = body.access_token; - if (access_token) { - var content = note.content; - var title = models.Note.decodeTitle(note.title); - var filename = title.replace('/', ' ') + '.md'; - var gist = { - "files": {} - }; - gist.files[filename] = { - "content": content - }; - var gist_url = "https://api.github.com/gists"; - request({ - url: gist_url, - headers: { - 'User-Agent': 'HackMD', - 'Authorization': 'token ' + access_token - }, - method: "POST", - json: gist - }, function (error, httpResponse, body) { - if (!error && httpResponse.statusCode == 201) { - res.setHeader('referer', ''); - res.redirect(body.html_url); - } else { - return response.errorForbidden(res); - } - }); - } else { - return response.errorForbidden(res); - } +function githubActionGist (req, res, note) { + var code = req.query.code + var state = req.query.state + if (!code || !state) { + return response.errorForbidden(res) + } else { + var data = { + client_id: config.github.clientID, + client_secret: config.github.clientSecret, + code: code, + state: state + } + var authUrl = 'https://github.com/login/oauth/access_token' + request({ + url: authUrl, + method: 'POST', + json: data + }, function (error, httpResponse, body) { + if (!error && httpResponse.statusCode === 200) { + var accessToken = body.access_token + if (accessToken) { + var content = note.content + var title = models.Note.decodeTitle(note.title) + var filename = title.replace('/', ' ') + '.md' + var gist = { + 'files': {} + } + gist.files[filename] = { + 'content': content + } + var gistUrl = 'https://api.github.com/gists' + request({ + url: gistUrl, + headers: { + 'User-Agent': 'HackMD', + 'Authorization': 'token ' + accessToken + }, + method: 'POST', + json: gist + }, function (error, httpResponse, body) { + if (!error && httpResponse.statusCode === 201) { + res.setHeader('referer', '') + res.redirect(body.html_url) } else { - return response.errorForbidden(res); + return response.errorForbidden(res) } - }) - } + }) + } else { + return response.errorForbidden(res) + } + } else { + return response.errorForbidden(res) + } + }) + } } -function gitlabActions(req, res, next) { - var noteId = req.params.noteId; - findNote(req, res, function (note) { - var action = req.params.action; - switch (action) { - case "projects": - gitlabActionProjects(req, res, note); - break; - default: - res.redirect(config.serverurl + '/' + noteId); - break; - } - }); +function gitlabActions (req, res, next) { + var noteId = req.params.noteId + findNote(req, res, function (note) { + var action = req.params.action + switch (action) { + case 'projects': + gitlabActionProjects(req, res, note) + break + default: + res.redirect(config.serverurl + '/' + noteId) + break + } + }) } -function gitlabActionProjects(req, res, note) { - if (req.isAuthenticated()) { - models.User.findOne({ - where: { - id: req.user.id - } - }).then(function (user) { - if (!user) - return response.errorNotFound(res); - var ret = { baseURL: config.gitlab.baseURL }; - ret.accesstoken = user.accessToken; - ret.profileid = user.profileid; - request( +function gitlabActionProjects (req, res, note) { + if (req.isAuthenticated()) { + models.User.findOne({ + where: { + id: req.user.id + } + }).then(function (user) { + if (!user) { return response.errorNotFound(res) } + var ret = { baseURL: config.gitlab.baseURL } + ret.accesstoken = user.accessToken + ret.profileid = user.profileid + request( config.gitlab.baseURL + '/api/v3/projects?access_token=' + user.accessToken, - function(error, httpResponse, body) { - if (!error && httpResponse.statusCode == 200) { - ret.projects = JSON.parse(body); - return res.send(ret); - } else { - return res.send(ret); - } + function (error, httpResponse, body) { + if (!error && httpResponse.statusCode === 200) { + ret.projects = JSON.parse(body) + return res.send(ret) + } else { + return res.send(ret) + } } - ); - }).catch(function (err) { - logger.error('gitlab action projects failed: ' + err); - return response.errorInternalError(res); - }); - } else { - return response.errorForbidden(res); - } + ) + }).catch(function (err) { + logger.error('gitlab action projects failed: ' + err) + return response.errorInternalError(res) + }) + } else { + return response.errorForbidden(res) + } } -function showPublishSlide(req, res, next) { - var include = [{ - model: models.User, - as: "owner" - }, { - model: models.User, - as: "lastchangeuser" - }]; - findNote(req, res, function (note) { - // force to use short id - var shortid = req.params.shortid; - if ((note.alias && shortid != note.alias) || (!note.alias && shortid != note.shortid)) - return res.redirect(config.serverurl + "/p/" + (note.alias || note.shortid)); - note.increment('viewcount').then(function (note) { - if (!note) { - return response.errorNotFound(res); - } - var body = note.content; - var extracted = models.Note.extractMeta(body); - markdown = extracted.markdown; - meta = models.Note.parseMeta(extracted.meta); - var createtime = note.createdAt; - var updatetime = note.lastchangeAt; - var title = models.Note.decodeTitle(note.title); - title = models.Note.generateWebTitle(meta.title || title); - var origin = config.serverurl; - var data = { - title: title, - description: meta.description || (markdown ? models.Note.generateDescription(markdown) : null), - viewcount: note.viewcount, - createtime: createtime, - updatetime: updatetime, - url: origin, - body: markdown, - meta: JSON.stringify(extracted.meta), - useCDN: config.usecdn, - owner: note.owner ? note.owner.id : null, - ownerprofile: note.owner ? models.User.getProfile(note.owner) : null, - lastchangeuser: note.lastchangeuser ? note.lastchangeuser.id : null, - lastchangeuserprofile: note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null, - robots: meta.robots || false, //default allow robots - GA: meta.GA, - disqus: meta.disqus - }; - return renderPublishSlide(data, res); - }).catch(function (err) { - logger.error(err); - return response.errorInternalError(res); - }); - }, include); +function showPublishSlide (req, res, next) { + var include = [{ + model: models.User, + as: 'owner' + }, { + model: models.User, + as: 'lastchangeuser' + }] + findNote(req, res, function (note) { + // force to use short id + var shortid = req.params.shortid + if ((note.alias && shortid !== note.alias) || (!note.alias && shortid !== note.shortid)) { return res.redirect(config.serverurl + '/p/' + (note.alias || note.shortid)) } + note.increment('viewcount').then(function (note) { + if (!note) { + return response.errorNotFound(res) + } + var body = note.content + var extracted = models.Note.extractMeta(body) + var markdown = extracted.markdown + var meta = models.Note.parseMeta(extracted.meta) + var createtime = note.createdAt + var updatetime = note.lastchangeAt + var title = models.Note.decodeTitle(note.title) + title = models.Note.generateWebTitle(meta.title || title) + var origin = config.serverurl + var data = { + title: title, + description: meta.description || (markdown ? models.Note.generateDescription(markdown) : null), + viewcount: note.viewcount, + createtime: createtime, + updatetime: updatetime, + url: origin, + body: markdown, + meta: JSON.stringify(extracted.meta), + useCDN: config.usecdn, + owner: note.owner ? note.owner.id : null, + ownerprofile: note.owner ? models.User.getProfile(note.owner) : null, + lastchangeuser: note.lastchangeuser ? note.lastchangeuser.id : null, + lastchangeuserprofile: note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null, + robots: meta.robots || false, // default allow robots + GA: meta.GA, + disqus: meta.disqus + } + return renderPublishSlide(data, res) + }).catch(function (err) { + logger.error(err) + return response.errorInternalError(res) + }) + }, include) } -function renderPublishSlide(data, res) { - res.set({ - 'Cache-Control': 'private' // only cache by client - }); - res.render(config.slidepath, data); +function renderPublishSlide (data, res) { + res.set({ + 'Cache-Control': 'private' // only cache by client + }) + res.render(config.slidepath, data) } -module.exports = response; +module.exports = response diff --git a/lib/workers/dmpWorker.js b/lib/workers/dmpWorker.js index 8e69636e..6a1da981 100644 --- a/lib/workers/dmpWorker.js +++ b/lib/workers/dmpWorker.js @@ -1,140 +1,137 @@ // external modules -var DiffMatchPatch = require('diff-match-patch'); -var dmp = new DiffMatchPatch(); +var DiffMatchPatch = require('diff-match-patch') +var dmp = new DiffMatchPatch() // core -var config = require("../config.js"); -var logger = require("../logger.js"); +var config = require('../config.js') +var logger = require('../logger.js') -process.on('message', function(data) { - if (!data || !data.msg || !data.cacheKey) { - return logger.error('dmp worker error: not enough data'); - } - switch (data.msg) { - case 'create patch': - if (!data.hasOwnProperty('lastDoc') || !data.hasOwnProperty('currDoc')) { - return logger.error('dmp worker error: not enough data on create patch'); - } - try { - var patch = createPatch(data.lastDoc, data.currDoc); - process.send({ - msg: 'check', - result: patch, - cacheKey: data.cacheKey - }); - } catch (err) { - logger.error('dmp worker error', err); - process.send({ - msg: 'error', - error: err, - cacheKey: data.cacheKey - }); - } - break; - case 'get revision': - if (!data.hasOwnProperty('revisions') || !data.hasOwnProperty('count')) { - return logger.error('dmp worker error: not enough data on get revision'); - } - try { - var result = getRevision(data.revisions, data.count); - process.send({ - msg: 'check', - result: result, - cacheKey: data.cacheKey - }); - } catch (err) { - logger.error('dmp worker error', err); - process.send({ - msg: 'error', - error: err, - cacheKey: data.cacheKey - }); - } - break; - } -}); +process.on('message', function (data) { + if (!data || !data.msg || !data.cacheKey) { + return logger.error('dmp worker error: not enough data') + } + switch (data.msg) { + case 'create patch': + if (!data.hasOwnProperty('lastDoc') || !data.hasOwnProperty('currDoc')) { + return logger.error('dmp worker error: not enough data on create patch') + } + try { + var patch = createPatch(data.lastDoc, data.currDoc) + process.send({ + msg: 'check', + result: patch, + cacheKey: data.cacheKey + }) + } catch (err) { + logger.error('dmp worker error', err) + process.send({ + msg: 'error', + error: err, + cacheKey: data.cacheKey + }) + } + break + case 'get revision': + if (!data.hasOwnProperty('revisions') || !data.hasOwnProperty('count')) { + return logger.error('dmp worker error: not enough data on get revision') + } + try { + var result = getRevision(data.revisions, data.count) + process.send({ + msg: 'check', + result: result, + cacheKey: data.cacheKey + }) + } catch (err) { + logger.error('dmp worker error', err) + process.send({ + msg: 'error', + error: err, + cacheKey: data.cacheKey + }) + } + break + } +}) -function createPatch(lastDoc, currDoc) { - var ms_start = (new Date()).getTime(); - var diff = dmp.diff_main(lastDoc, currDoc); - var patch = dmp.patch_make(lastDoc, diff); - patch = dmp.patch_toText(patch); - var ms_end = (new Date()).getTime(); - if (config.debug) { - logger.info(patch); - logger.info((ms_end - ms_start) + 'ms'); - } - return patch; +function createPatch (lastDoc, currDoc) { + var msStart = (new Date()).getTime() + var diff = dmp.diff_main(lastDoc, currDoc) + var patch = dmp.patch_make(lastDoc, diff) + patch = dmp.patch_toText(patch) + var msEnd = (new Date()).getTime() + if (config.debug) { + logger.info(patch) + logger.info((msEnd - msStart) + 'ms') + } + return patch } -function getRevision(revisions, count) { - var ms_start = (new Date()).getTime(); - var startContent = null; - var lastPatch = []; - var applyPatches = []; - var authorship = []; - if (count <= Math.round(revisions.length / 2)) { - // start from top to target - for (var i = 0; i < count; i++) { - var revision = revisions[i]; - if (i == 0) { - startContent = revision.content || revision.lastContent; - } - if (i != count - 1) { - var patch = dmp.patch_fromText(revision.patch); - applyPatches = applyPatches.concat(patch); - } - lastPatch = revision.patch; - authorship = revision.authorship; - } - // swap DIFF_INSERT and DIFF_DELETE to achieve unpatching - for (var i = 0, l = applyPatches.length; i < l; i++) { - for (var j = 0, m = applyPatches[i].diffs.length; j < m; j++) { - var diff = applyPatches[i].diffs[j]; - if (diff[0] == DiffMatchPatch.DIFF_INSERT) - diff[0] = DiffMatchPatch.DIFF_DELETE; - else if (diff[0] == DiffMatchPatch.DIFF_DELETE) - diff[0] = DiffMatchPatch.DIFF_INSERT; - } - } - } else { - // start from bottom to target - var l = revisions.length - 1; - for (var i = l; i >= count - 1; i--) { - var revision = revisions[i]; - if (i == l) { - startContent = revision.lastContent; - authorship = revision.authorship; - } - if (revision.patch) { - var patch = dmp.patch_fromText(revision.patch); - applyPatches = applyPatches.concat(patch); - } - lastPatch = revision.patch; - authorship = revision.authorship; - } +function getRevision (revisions, count) { + var msStart = (new Date()).getTime() + var startContent = null + var lastPatch = [] + var applyPatches = [] + var authorship = [] + if (count <= Math.round(revisions.length / 2)) { + // start from top to target + for (let i = 0; i < count; i++) { + let revision = revisions[i] + if (i === 0) { + startContent = revision.content || revision.lastContent + } + if (i !== count - 1) { + let patch = dmp.patch_fromText(revision.patch) + applyPatches = applyPatches.concat(patch) + } + lastPatch = revision.patch + authorship = revision.authorship } - try { - var finalContent = dmp.patch_apply(applyPatches, startContent)[0]; - } catch (err) { - throw new Error(err); + // swap DIFF_INSERT and DIFF_DELETE to achieve unpatching + for (let i = 0, l = applyPatches.length; i < l; i++) { + for (let j = 0, m = applyPatches[i].diffs.length; j < m; j++) { + var diff = applyPatches[i].diffs[j] + if (diff[0] === DiffMatchPatch.DIFF_INSERT) { diff[0] = DiffMatchPatch.DIFF_DELETE } else if (diff[0] === DiffMatchPatch.DIFF_DELETE) { diff[0] = DiffMatchPatch.DIFF_INSERT } + } } - var data = { - content: finalContent, - patch: dmp.patch_fromText(lastPatch), - authorship: authorship - }; - var ms_end = (new Date()).getTime(); - if (config.debug) { - logger.info((ms_end - ms_start) + 'ms'); + } else { + // start from bottom to target + var l = revisions.length - 1 + for (var i = l; i >= count - 1; i--) { + let revision = revisions[i] + if (i === l) { + startContent = revision.lastContent + authorship = revision.authorship + } + if (revision.patch) { + let patch = dmp.patch_fromText(revision.patch) + applyPatches = applyPatches.concat(patch) + } + lastPatch = revision.patch + authorship = revision.authorship } - return data; + } + try { + var finalContent = dmp.patch_apply(applyPatches, startContent)[0] + } catch (err) { + throw new Error(err) + } + var data = { + content: finalContent, + patch: dmp.patch_fromText(lastPatch), + authorship: authorship + } + var msEnd = (new Date()).getTime() + if (config.debug) { + logger.info((msEnd - msStart) + 'ms') + } + return data } // log uncaught exception process.on('uncaughtException', function (err) { - logger.error('An uncaught exception has occured.'); - logger.error(err); - logger.error('Process will exit now.'); - process.exit(1); -});
\ No newline at end of file + logger.error('An uncaught exception has occured.') + logger.error(err) + logger.error('Process will exit now.') + process.exit(1) +}) diff --git a/package.json b/package.json index a179d93e..2012dbdf 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "app.js", "license": "MIT", "scripts": { - "test": "npm run-script lint", + "test": "node ./node_modules/standard/bin/cmd.js && npm run-script lint", "lint": "eslint .", "dev": "webpack --config webpack.config.js --progress --colors --watch", "build": "webpack --config webpack.production.js --progress --colors", @@ -165,8 +165,15 @@ "optimize-css-assets-webpack-plugin": "^1.3.0", "script-loader": "^0.7.0", "style-loader": "^0.13.1", + "standard": "^9.0.1", "url-loader": "^0.5.7", "webpack": "^1.14.0", "webpack-parallel-uglify-plugin": "^0.2.0" + }, + "standard": { + "ignore": [ + "lib/ot", + "public/vendor" + ] } } diff --git a/public/js/cover.js b/public/js/cover.js index bc6e73f9..a45a1c13 100644 --- a/public/js/cover.js +++ b/public/js/cover.js @@ -1,7 +1,10 @@ -require('./locale'); +/* eslint-env browser, jquery */ +/* global moment, serverurl */ -require('../css/cover.css'); -require('../css/site.css'); +require('./locale') + +require('../css/cover.css') +require('../css/site.css') import { checkIfAuth, @@ -9,7 +12,7 @@ import { getLoginState, resetCheckAuth, setloginStateChangeEvent -} from './lib/common/login'; +} from './lib/common/login' import { clearDuplicatedHistory, @@ -23,411 +26,403 @@ import { removeHistory, saveHistory, saveStorageHistoryToServer -} from './history'; +} from './history' -import { saveAs } from 'file-saver'; -import List from 'list.js'; -import S from 'string'; +import { saveAs } from 'file-saver' +import List from 'list.js' +import S from 'string' 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-close fa fa-close fa-fw" data-toggle="modal" data-target=".delete-modal"></div>\ - <div class="content">\ - <h4 class="text"></h4>\ - <p>\ - <i><i class="fa fa-clock-o"></i> visited </i><i class="fromNow"></i>\ - <br>\ - <i class="timestamp" style="display:none;"></i>\ - <i class="time"></i>\ - </p>\ - <p class="tags"></p>\ - </div>\ - </div>\ - </a>\ - </li>', - page: 18, - plugins: [ - ListPagination({ - outerWindow: 1 - }) - ] -}; -const historyList = new List('history', options); - -migrateHistoryFromTempCallback = pageInit; -setloginStateChangeEvent(pageInit); - -pageInit(); - -function pageInit() { - checkIfAuth( + 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-close fa fa-close fa-fw" data-toggle="modal" data-target=".delete-modal"></div>' + + '<div class="content">' + + '<h4 class="text"></h4>' + + '<p>' + + '<i><i class="fa fa-clock-o"></i> visited </i><i class="fromNow"></i>' + + '<br>' + + '<i class="timestamp" style="display:none;"></i>' + + '<i class="time"></i>' + + '</p>' + + '<p class="tags"></p>' + + '</div>' + + '</div>' + + '</a>' + + '</li>', + page: 18, + plugins: [ + window.ListPagination({ + outerWindow: 1 + }) + ] +} +const historyList = new List('history', options) + +window.migrateHistoryFromTempCallback = pageInit +setloginStateChangeEvent(pageInit) + +pageInit() + +function pageInit () { + checkIfAuth( data => { - $('.ui-signin').hide(); - $('.ui-or').hide(); - $('.ui-welcome').show(); - if (data.photo) $('.ui-avatar').prop('src', data.photo).show(); - else $('.ui-avatar').prop('src', '').hide(); - $('.ui-name').html(data.name); - $('.ui-signout').show(); - $(".ui-history").click(); - parseServerToHistory(historyList, parseHistoryCallback); + $('.ui-signin').hide() + $('.ui-or').hide() + $('.ui-welcome').show() + if (data.photo) $('.ui-avatar').prop('src', data.photo).show() + else $('.ui-avatar').prop('src', '').hide() + $('.ui-name').html(data.name) + $('.ui-signout').show() + $('.ui-history').click() + parseServerToHistory(historyList, parseHistoryCallback) }, () => { - $('.ui-signin').show(); - $('.ui-or').show(); - $('.ui-welcome').hide(); - $('.ui-avatar').prop('src', '').hide(); - $('.ui-name').html(''); - $('.ui-signout').hide(); - parseStorageToHistory(historyList, parseHistoryCallback); + $('.ui-signin').show() + $('.ui-or').show() + $('.ui-welcome').hide() + $('.ui-avatar').prop('src', '').hide() + $('.ui-name').html('') + $('.ui-signout').hide() + parseStorageToHistory(historyList, parseHistoryCallback) } - ); + ) } -$(".masthead-nav li").click(function () { - $(this).siblings().removeClass("active"); - $(this).addClass("active"); -}); +$('.masthead-nav li').click(function () { + $(this).siblings().removeClass('active') + $(this).addClass('active') +}) // prevent empty link change hash $('a[href="#"]').click(function (e) { - e.preventDefault(); -}); - -$(".ui-home").click(function (e) { - if (!$("#home").is(':visible')) { - $(".section:visible").hide(); - $("#home").fadeIn(); - } -}); - -$(".ui-history").click(() => { - if (!$("#history").is(':visible')) { - $(".section:visible").hide(); - $("#history").fadeIn(); - } -}); - -function checkHistoryList() { - if ($("#history-list").children().length > 0) { - $('.pagination').show(); - $(".ui-nohistory").hide(); - $(".ui-import-from-browser").hide(); - } else if ($("#history-list").children().length == 0) { - $('.pagination').hide(); - $(".ui-nohistory").slideDown(); - getStorageHistory(data => { - if (data && data.length > 0 && getLoginState() && historyList.items.length == 0) { - $(".ui-import-from-browser").slideDown(); - } - }); - } + e.preventDefault() +}) + +$('.ui-home').click(function (e) { + if (!$('#home').is(':visible')) { + $('.section:visible').hide() + $('#home').fadeIn() + } +}) + +$('.ui-history').click(() => { + if (!$('#history').is(':visible')) { + $('.section:visible').hide() + $('#history').fadeIn() + } +}) + +function checkHistoryList () { + if ($('#history-list').children().length > 0) { + $('.pagination').show() + $('.ui-nohistory').hide() + $('.ui-import-from-browser').hide() + } else if ($('#history-list').children().length === 0) { + $('.pagination').hide() + $('.ui-nohistory').slideDown() + getStorageHistory(data => { + if (data && data.length > 0 && getLoginState() && historyList.items.length === 0) { + $('.ui-import-from-browser').slideDown() + } + }) + } } -function parseHistoryCallback(list, notehistory) { - checkHistoryList(); - //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; - } - } +function parseHistoryCallback (list, notehistory) { + checkHistoryList() + // 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 } - }); + } + } + }) // parse filter 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 (let j = 0; j < tags.length; j++) { - //push info filtertags if not found - let found = false; - if (filtertags.includes(tags[j])) - found = true; - if (!found) - filtertags.push(tags[j]); - } - } + 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 (let j = 0; j < tags.length; j++) { + // push info filtertags if not found + let found = false + if (filtertags.includes(tags[j])) { found = true } + if (!found) { filtertags.push(tags[j]) } + } } - buildTagsFilter(filtertags); + } + buildTagsFilter(filtertags) } // update items whenever list updated historyList.on('updated', e => { - for (let i = 0, l = e.items.length; i < l; i++) { - const item = e.items[i]; - if (item.visible()) { - 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}`); - //parse pinned - if (values.pinned) { - pin.addClass('active'); - } else { - pin.removeClass('active'); - } - //parse tags - const tags = values.tags; - if (tags && tags.length > 0 && tagsEl.children().length <= 0) { - 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>`); - } - tagsEl.html(labels.join(' ')); - } + for (let i = 0, l = e.items.length; i < l; i++) { + const item = e.items[i] + if (item.visible()) { + 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}`) + // parse pinned + if (values.pinned) { + pin.addClass('active') + } else { + pin.removeClass('active') + } + // parse tags + const tags = values.tags + if (tags && tags.length > 0 && tagsEl.children().length <= 0) { + 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>`) } + tagsEl.html(labels.join(' ')) + } } - $(".ui-history-close").off('click'); - $(".ui-history-close").on('click', historyCloseClick); - $(".ui-history-pin").off('click'); - $(".ui-history-pin").on('click', historyPinClick); -}); - -function historyCloseClick(e) { - e.preventDefault(); - 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}`); - clearHistory = false; - deleteId = id; + } + $('.ui-history-close').off('click') + $('.ui-history-close').on('click', historyCloseClick) + $('.ui-history-pin').off('click') + $('.ui-history-pin').on('click', historyPinClick) +}) + +function historyCloseClick (e) { + e.preventDefault() + 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}`) + clearHistory = false + deleteId = id } -function historyPinClick(e) { - e.preventDefault(); - 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; - } else { - pinned = false; - item._values.pinned = false; - } - checkIfAuth(() => { - postHistoryToServer(id, { - pinned - }, (err, result) => { - if (!err) { - if (pinned) - $this.addClass('active'); - else - $this.removeClass('active'); - } - }); - }, () => { - getHistory(notehistory => { - for(let i = 0; i < notehistory.length; i++) { - if (notehistory[i].id == id) { - notehistory[i].pinned = pinned; - break; - } - } - saveHistory(notehistory); - if (pinned) - $this.addClass('active'); - else - $this.removeClass('active'); - }); - }); +function historyPinClick (e) { + e.preventDefault() + 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 + } else { + pinned = false + item._values.pinned = false + } + checkIfAuth(() => { + postHistoryToServer(id, { + pinned + }, (err, result) => { + if (!err) { + if (pinned) { $this.addClass('active') } else { $this.removeClass('active') } + } + }) + }, () => { + getHistory(notehistory => { + for (let i = 0; i < notehistory.length; i++) { + if (notehistory[i].id === id) { + notehistory[i].pinned = pinned + break + } + } + saveHistory(notehistory) + if (pinned) { $this.addClass('active') } else { $this.removeClass('active') } + }) + }) } -//auto update item fromNow every minutes -setInterval(updateItemFromNow, 60000); +// auto update item fromNow every minutes +setInterval(updateItemFromNow, 60000) -function updateItemFromNow() { - 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()); - } +function updateItemFromNow () { + 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()) + } } -var clearHistory = false; -var deleteId = null; - -function deleteHistory() { - checkIfAuth(() => { - deleteServerHistory(deleteId, (err, result) => { - if (!err) { - if (clearHistory) { - historyList.clear(); - checkHistoryList(); - } else { - historyList.remove('id', deleteId); - checkHistoryList(); - } - } - $('.delete-modal').modal('hide'); - deleteId = null; - clearHistory = false; - }); - }, () => { +var clearHistory = false +var deleteId = null + +function deleteHistory () { + checkIfAuth(() => { + deleteServerHistory(deleteId, (err, result) => { + if (!err) { if (clearHistory) { - saveHistory([]); - historyList.clear(); - checkHistoryList(); - deleteId = null; + historyList.clear() + checkHistoryList() } else { - if (!deleteId) return; - getHistory(notehistory => { - const newnotehistory = removeHistory(deleteId, notehistory); - saveHistory(newnotehistory); - historyList.remove('id', deleteId); - checkHistoryList(); - deleteId = null; - }); + historyList.remove('id', deleteId) + checkHistoryList() } - $('.delete-modal').modal('hide'); - clearHistory = false; - }); + } + $('.delete-modal').modal('hide') + deleteId = null + clearHistory = false + }) + }, () => { + if (clearHistory) { + saveHistory([]) + historyList.clear() + checkHistoryList() + deleteId = null + } else { + if (!deleteId) return + getHistory(notehistory => { + const newnotehistory = removeHistory(deleteId, notehistory) + saveHistory(newnotehistory) + historyList.remove('id', deleteId) + checkHistoryList() + deleteId = null + }) + } + $('.delete-modal').modal('hide') + clearHistory = false + }) } -$(".ui-delete-modal-confirm").click(() => { - deleteHistory(); -}); - -$(".ui-import-from-browser").click(() => { - saveStorageHistoryToServer(() => { - parseStorageToHistory(historyList, parseHistoryCallback); - }); -}); - -$(".ui-save-history").click(() => { +$('.ui-delete-modal-confirm').click(() => { + deleteHistory() +}) + +$('.ui-import-from-browser').click(() => { + saveStorageHistoryToServer(() => { + parseStorageToHistory(historyList, parseHistoryCallback) + }) +}) + +$('.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')}`, true) + }) +}) + +$('.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(data => { - const history = JSON.stringify(data); - const blob = new Blob([history], { - type: "application/json;charset=utf-8" - }); - saveAs(blob, `hackmd_history_${moment().format('YYYYMMDDHHmmss')}`, true); - }); -}); - -$(".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(data => { - let mergedata = data.concat(notehistory); - mergedata = clearDuplicatedHistory(mergedata); - saveHistory(mergedata); - parseHistory(historyList, parseHistoryCallback); - }); - $(".ui-open-history").replaceWith($(".ui-open-history").val('').clone(true)); - }; - reader.readAsText(file); -}); - -$(".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(() => { - const lastTags = $(".ui-use-tags").select2('val'); - $(".ui-use-tags").select2('val', ''); - historyList.filter(); - const lastKeyword = $('.search').val(); - $('.search').val(''); - historyList.search(); - $('#history-list').slideUp('fast'); - $('.pagination').hide(); - - resetCheckAuth(); - historyList.clear(); - parseHistory(historyList, (list, notehistory) => { - parseHistoryCallback(list, notehistory); - $(".ui-use-tags").select2('val', lastTags); - $(".ui-use-tags").trigger('change'); - historyList.search(lastKeyword); - $('.search').val(lastKeyword); - checkHistoryList(); - $('#history-list').slideDown('fast'); - }); -}); - -$(".ui-logout").click(() => { - clearLoginState(); - location.href = `${serverurl}/logout`; -}); - -let filtertags = []; -$(".ui-use-tags").select2({ - placeholder: $(".ui-use-tags").attr('placeholder'), - multiple: true, - data() { - return { - results: filtertags - }; + let mergedata = data.concat(notehistory) + mergedata = clearDuplicatedHistory(mergedata) + saveHistory(mergedata) + parseHistory(historyList, parseHistoryCallback) + }) + $('.ui-open-history').replaceWith($('.ui-open-history').val('').clone(true)) + } + reader.readAsText(file) +}) + +$('.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(() => { + const lastTags = $('.ui-use-tags').select2('val') + $('.ui-use-tags').select2('val', '') + historyList.filter() + const lastKeyword = $('.search').val() + $('.search').val('') + historyList.search() + $('#history-list').slideUp('fast') + $('.pagination').hide() + + resetCheckAuth() + historyList.clear() + parseHistory(historyList, (list, notehistory) => { + parseHistoryCallback(list, notehistory) + $('.ui-use-tags').select2('val', lastTags) + $('.ui-use-tags').trigger('change') + historyList.search(lastKeyword) + $('.search').val(lastKeyword) + checkHistoryList() + $('#history-list').slideDown('fast') + }) +}) + +$('.ui-logout').click(() => { + clearLoginState() + location.href = `${serverurl}/logout` +}) + +let filtertags = [] +$('.ui-use-tags').select2({ + placeholder: $('.ui-use-tags').attr('placeholder'), + multiple: true, + data () { + return { + results: filtertags } -}); -$('.select2-input').css('width', 'inherit'); -buildTagsFilter([]); - -function buildTagsFilter(tags) { - for (let i = 0; i < tags.length; i++) - tags[i] = { - id: i, - text: S(tags[i]).unescapeHTML().s - }; - filtertags = tags; -} -$(".ui-use-tags").on('change', function () { - 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(item => { - const values = item.values(); - if (!values.tags) return false; - let found = false; - for (let i = 0; i < tags.length; i++) { - if (values.tags.includes(tags[i])) { - found = true; - break; - } - } - return found; - }); - } else { - historyList.filter(); + } +}) +$('.select2-input').css('width', 'inherit') +buildTagsFilter([]) + +function buildTagsFilter (tags) { + for (let i = 0; i < tags.length; i++) { + tags[i] = { + id: i, + text: S(tags[i]).unescapeHTML().s } - checkHistoryList(); -}); + } + filtertags = tags +} +$('.ui-use-tags').on('change', function () { + 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(item => { + const values = item.values() + if (!values.tags) return false + let found = false + for (let i = 0; i < tags.length; i++) { + if (values.tags.includes(tags[i])) { + found = true + break + } + } + return found + }) + } else { + historyList.filter() + } + checkHistoryList() +}) $('.search').keyup(() => { - checkHistoryList(); -}); + checkHistoryList() +}) diff --git a/public/js/extra.js b/public/js/extra.js index a3e840d2..844d52c6 100644 --- a/public/js/extra.js +++ b/public/js/extra.js @@ -1,1150 +1,1140 @@ -require('prismjs/themes/prism.css'); -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'); - -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"); - -//auto update last change -window.createtime = null; -window.lastchangetime = null; +/* eslint-env browser, jquery */ +/* global moment, serverurl */ + +require('prismjs/themes/prism.css') +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') + +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') + +// auto update last change +window.createtime = null +window.lastchangetime = null window.lastchangeui = { - status: $(".ui-status-lastchange"), - time: $(".ui-lastchange"), - user: $(".ui-lastchangeuser"), - nouser: $(".ui-no-lastchangeuser") + status: $('.ui-status-lastchange'), + time: $('.ui-lastchange'), + user: $('.ui-lastchangeuser'), + nouser: $('.ui-no-lastchangeuser') } -const ownerui = $(".ui-owner"); +const ownerui = $('.ui-owner') -export function updateLastChange() { - if (!lastchangeui) return; - if (createtime) { - if (createtime && !lastchangetime) { - lastchangeui.status.text('created'); - } else { - lastchangeui.status.text('changed'); - } - const time = lastchangetime || createtime; - lastchangeui.time.html(moment(time).fromNow()); - lastchangeui.time.attr('title', moment(time).format('llll')); +export function updateLastChange () { + if (!window.lastchangeui) return + if (window.createtime) { + if (window.createtime && !window.lastchangetime) { + window.lastchangeui.status.text('created') + } else { + window.lastchangeui.status.text('changed') } + const time = window.lastchangetime || window.createtime + window.lastchangeui.time.html(moment(time).fromNow()) + window.lastchangeui.time.attr('title', moment(time).format('llll')) + } } -setInterval(updateLastChange, 60000); - -window.lastchangeuser = null; -window.lastchangeuserprofile = null; - -export function updateLastChangeUser() { - if (lastchangeui) { - if (lastchangeuser && lastchangeuserprofile) { - const icon = lastchangeui.user.children('i'); - icon.attr('title', lastchangeuserprofile.name).tooltip('fixTitle'); - if (lastchangeuserprofile.photo) - icon.attr('style', `background-image:url(${lastchangeuserprofile.photo})`); - lastchangeui.user.show(); - lastchangeui.nouser.hide(); - } else { - lastchangeui.user.hide(); - lastchangeui.nouser.show(); - } +setInterval(updateLastChange, 60000) + +window.lastchangeuser = null +window.lastchangeuserprofile = null + +export function updateLastChangeUser () { + if (window.lastchangeui) { + if (window.lastchangeuser && window.lastchangeuserprofile) { + const icon = window.lastchangeui.user.children('i') + icon.attr('title', window.lastchangeuserprofile.name).tooltip('fixTitle') + if (window.lastchangeuserprofile.photo) { icon.attr('style', `background-image:url(${window.lastchangeuserprofile.photo})`) } + window.lastchangeui.user.show() + window.lastchangeui.nouser.hide() + } else { + window.lastchangeui.user.hide() + window.lastchangeui.nouser.show() } + } } -window.owner = null; -window.ownerprofile = null; - -export function updateOwner() { - if (ownerui) { - if (owner && ownerprofile && owner !== lastchangeuser) { - const icon = ownerui.children('i'); - icon.attr('title', ownerprofile.name).tooltip('fixTitle'); - const styleString = `background-image:url(${ownerprofile.photo})`; - if (ownerprofile.photo && icon.attr('style') !== styleString) - icon.attr('style', styleString); - ownerui.show(); - } else { - ownerui.hide(); - } +window.owner = null +window.ownerprofile = null + +export function updateOwner () { + if (window.ownerui) { + if (window.owner && window.ownerprofile && window.owner !== window.lastchangeuser) { + const icon = ownerui.children('i') + icon.attr('title', window.ownerprofile.name).tooltip('fixTitle') + const styleString = `background-image:url(${window.ownerprofile.photo})` + if (window.ownerprofile.photo && icon.attr('style') !== styleString) { icon.attr('style', styleString) } + ownerui.show() + } else { + ownerui.hide() } + } } -//get title -function getTitle(view) { - let title = ""; - if (md && md.meta && md.meta.title && (typeof md.meta.title == "string" || typeof md.meta.title == "number")) { - title = md.meta.title; +// get title +function getTitle (view) { + let title = '' + if (md && md.meta && md.meta.title && (typeof md.meta.title === 'string' || typeof md.meta.title === 'number')) { + title = md.meta.title + } else { + const h1s = view.find('h1') + if (h1s.length > 0) { + title = h1s.first().text() } else { - const h1s = view.find("h1"); - if (h1s.length > 0) { - title = h1s.first().text(); - } else { - title = null; - } + title = null } - return title; + } + return title } -//render title -export function renderTitle(view) { - let title = getTitle(view); - if (title) { - title += ' - HackMD'; - } else { - title = 'HackMD - Collaborative markdown notes'; - } - return title; +// render title +export function renderTitle (view) { + let title = getTitle(view) + if (title) { + title += ' - HackMD' + } else { + title = 'HackMD - Collaborative markdown notes' + } + return title } -//render filename -export function renderFilename(view) { - let filename = getTitle(view); - if (!filename) { - filename = 'Untitled'; - } - return filename; +// render filename +export function renderFilename (view) { + let filename = getTitle(view) + if (!filename) { + filename = 'Untitled' + } + return filename } // render tags -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")) { - const metaTags = (`${md.meta.tags}`).split(','); - for (var i = 0; i < metaTags.length; i++) { - const text = metaTags[i].trim(); - if (text) rawtags.push(text); - } - } else { - view.find('h6').each((key, value) => { - if (/^tags/gmi.test($(value).text())) { - const codes = $(value).find("code"); - for (let i = 0; i < codes.length; i++) { - const text = codes[i].innerHTML.trim(); - if (text) rawtags.push(text); - } - } - }); +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')) { + const metaTags = (`${md.meta.tags}`).split(',') + for (let i = 0; i < metaTags.length; i++) { + const text = metaTags[i].trim() + if (text) rawtags.push(text) } - for (var i = 0; i < rawtags.length; i++) { - let found = false; - for (let j = 0; j < tags.length; j++) { - if (tags[j] == rawtags[i]) { - found = true; - break; - } + } else { + view.find('h6').each((key, value) => { + if (/^tags/gmi.test($(value).text())) { + const codes = $(value).find('code') + for (let i = 0; i < codes.length; i++) { + const text = codes[i].innerHTML.trim() + if (text) rawtags.push(text) } - if (!found) - tags.push(rawtags[i]); + } + }) + } + for (let i = 0; i < rawtags.length; i++) { + let found = false + for (let j = 0; j < tags.length; j++) { + if (tags[j] === rawtags[i]) { + found = true + break + } } - return tags; + if (!found) { tags.push(rawtags[i]) } + } + return tags } -function slugifyWithUTF8(text) { - let newText = S(text.toLowerCase()).trim().stripTags().dasherize().s; - newText = newText.replace(/([\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\[\\\]\^\`\{\|\}\~])/g, ''); - return newText; +function slugifyWithUTF8 (text) { + let newText = S(text.toLowerCase()).trim().stripTags().dasherize().s + newText = newText.replace(/([!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~])/g, '') + return newText } -export function isValidURL(str) { - const 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 '(\\?[;&a-z\\d%_.~+=-]*)?' + // query string - '(\\#[-a-z\\d_]*)?$', 'i'); // fragment locator - if (!pattern.test(str)) { - return false; - } else { - return true; - } + '(\\#[-a-z\\d_]*)?$', 'i') // fragment locator + if (!pattern.test(str)) { + return false + } else { + return true + } } -//parse meta -export function parseMeta(md, edit, view, toc, tocAffix) { - let lang = null; - let dir = null; - let breaks = true; - if (md && md.meta) { - const meta = md.meta; - lang = meta.lang; - dir = meta.dir; - breaks = meta.breaks; - } - //text language - if (lang && typeof lang == "string") { - view.attr('lang', lang); - toc.attr('lang', lang); - tocAffix.attr('lang', lang); - if (edit) - edit.attr('lang', lang); - } else { - view.removeAttr('lang'); - toc.removeAttr('lang'); - tocAffix.removeAttr('lang'); - if (edit) - edit.removeAttr('lang', lang); - } - //text direction - if (dir && typeof dir == "string") { - view.attr('dir', dir); - toc.attr('dir', dir); - tocAffix.attr('dir', dir); - } else { - view.removeAttr('dir'); - toc.removeAttr('dir'); - tocAffix.removeAttr('dir'); - } - //breaks - if (typeof breaks === 'boolean' && !breaks) { - md.options.breaks = false; - } else { - md.options.breaks = true; - } +// parse meta +export function parseMeta (md, edit, view, toc, tocAffix) { + let lang = null + let dir = null + let breaks = true + if (md && md.meta) { + const meta = md.meta + lang = meta.lang + dir = meta.dir + breaks = meta.breaks + } + // text language + if (lang && typeof lang === 'string') { + view.attr('lang', lang) + toc.attr('lang', lang) + tocAffix.attr('lang', lang) + if (edit) { edit.attr('lang', lang) } + } else { + view.removeAttr('lang') + toc.removeAttr('lang') + tocAffix.removeAttr('lang') + if (edit) { edit.removeAttr('lang', lang) } + } + // text direction + if (dir && typeof dir === 'string') { + view.attr('dir', dir) + toc.attr('dir', dir) + tocAffix.attr('dir', dir) + } else { + view.removeAttr('dir') + toc.removeAttr('dir') + tocAffix.removeAttr('dir') + } + // breaks + if (typeof breaks === 'boolean' && !breaks) { + md.options.breaks = false + } else { + md.options.breaks = true + } } -window.viewAjaxCallback = null; - -//regex for extra tags -const spaceregex = /\s*/; -const notinhtmltagregex = /(?![^<]*>|[^<>]*<\/)/; -let coloregex = /\[color=([#|\(|\)|\s|\,|\w]*?)\]/; -coloregex = new RegExp(coloregex.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"); - -function replaceExtraTags(html) { - html = html.replace(coloregex, '<span class="color" data-color="$1"></span>'); - html = html.replace(nameandtimeregex, '<small><i class="fa fa-user"></i> $1 <i class="fa fa-clock-o"></i> $2</small>'); - html = html.replace(nameregex, '<small><i class="fa fa-user"></i> $1</small>'); - html = html.replace(timeregex, '<small><i class="fa fa-clock-o"></i> $1</small>'); - return html; +window.viewAjaxCallback = null + +// regex for extra tags +const spaceregex = /\s*/ +const notinhtmltagregex = /(?![^<]*>|[^<>]*<\/)/ +let coloregex = /\[color=([#|(|)|\s|,|\w]*?)\]/ +coloregex = new RegExp(coloregex.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') + +function replaceExtraTags (html) { + html = html.replace(coloregex, '<span class="color" data-color="$1"></span>') + html = html.replace(nameandtimeregex, '<small><i class="fa fa-user"></i> $1 <i class="fa fa-clock-o"></i> $2</small>') + html = html.replace(nameregex, '<small><i class="fa fa-user"></i> $1</small>') + html = html.replace(timeregex, '<small><i class="fa fa-clock-o"></i> $1</small>') + return html } -if (typeof mermaid !== 'undefined' && mermaid) mermaid.startOnLoad = false; +if (typeof window.mermaid !== 'undefined' && window.mermaid) window.mermaid.startOnLoad = false -//dynamic event or object binding here -export function finishView(view) { - //todo list - const lis = view.find('li.raw').removeClass("raw").sortByDepth().toArray(); +// dynamic event or object binding here +export function finishView (view) { + // todo list + 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; - 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>`); - 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 - const tag_color = $(li).closest('ul').find(".color"); - tag_color.each((key, value) => { - $(value).addClass('fa fa-tag').css('color', $(value).attr('data-color')); - }); + 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] } - - //youtube - view.find("div.youtube.raw").removeClass("raw") + html = replaceExtraTags(html) + li.innerHTML = html + let disabled = 'disabled' + if (typeof editor !== 'undefined' && window.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>`) + li.setAttribute('class', 'task-list-item') + } + if (typeof editor !== 'undefined' && window.havePermission()) { $(li).find('input').change(toggleTodoEvent) } + // color tag in list will convert it to tag icon with color + const tagColor = $(li).closest('ul').find('.color') + tagColor.each((key, value) => { + $(value).addClass('fa fa-tag').css('color', $(value).attr('data-color')) + }) + } + + // youtube + view.find('div.youtube.raw').removeClass('raw') .click(function () { - imgPlayiframe(this, '//www.youtube.com/embed/'); - }); - //vimeo - view.find("div.vimeo.raw").removeClass("raw") + imgPlayiframe(this, '//www.youtube.com/embed/') + }) + // vimeo + view.find('div.vimeo.raw').removeClass('raw') .click(function () { - imgPlayiframe(this, '//player.vimeo.com/video/'); + imgPlayiframe(this, '//player.vimeo.com/video/') }) .each((key, value) => { - $.ajax({ - type: 'GET', - url: `//vimeo.com/api/v2/video/${$(value).attr('data-videoid')}.json`, - jsonp: 'callback', - dataType: 'jsonp', - 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((key, value) => { - if ($(value).children().length == 0) - $(value).gist(viewAjaxCallback); - }); - //sequence diagram - const sequences = view.find("div.sequence-diagram.raw").removeClass("raw"); - sequences.each((key, value) => { - try { - var $value = $(value); - const $ele = $(value).parent().parent(); - - const sequence = $value; - sequence.sequenceDiagram({ - theme: 'simple' - }); - - $ele.addClass('sequence-diagram'); - $value.children().unwrap().unwrap(); - 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(); - $value.parent().append('<div class="alert alert-warning">' + err + '</div>'); - console.warn(err); - } - }); - //flowchart - const flow = view.find("div.flow-chart.raw").removeClass("raw"); - flow.each((key, value) => { - try { - var $value = $(value); - const $ele = $(value).parent().parent(); - - const chart = flowchart.parse($value.text()); - $value.html(''); - chart.drawSVG(value, { - 'line-width': 2, - 'fill': 'none', - 'font-size': '16px', - 'font-family': "'Andale Mono', monospace" - }); - - $ele.addClass('flow-chart'); - $value.children().unwrap().unwrap(); - } catch (err) { - $value.unwrap(); - $value.parent().append('<div class="alert alert-warning">' + err + '</div>'); - console.warn(err); - } - }); - //graphviz - var graphvizs = view.find("div.graphviz.raw").removeClass("raw"); - graphvizs.each(function (key, value) { - try { - 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) { - $value.unwrap(); - $value.parent().append('<div class="alert alert-warning">' + err + '</div>'); - console.warn(err); - } - }); - //mermaid - const mermaids = view.find("div.mermaid.raw").removeClass("raw"); - mermaids.each((key, value) => { - try { - var $value = $(value); - const $ele = $(value).closest('pre'); - - let mermaidError = null; - mermaid.parseError = (err, hash) => { - mermaidError = err; - }; - - if (mermaidAPI.parse($value.text())) { - $ele.addClass('mermaid'); - $ele.html($value.text()); - mermaid.init(undefined, $ele); - } else { - throw new Error(mermaidError); + $.ajax({ + type: 'GET', + url: `//vimeo.com/api/v2/video/${$(value).attr('data-videoid')}.json`, + jsonp: 'callback', + dataType: 'jsonp', + success (data) { + const thumbnailSrc = data[0].thumbnail_large + const image = `<img src="${thumbnailSrc}" />` + $(value).prepend(image) + if (window.viewAjaxCallback) window.viewAjaxCallback() } - } catch (err) { - $value.unwrap(); - $value.parent().append('<div class="alert alert-warning">' + err + '</div>'); - console.warn(err); - } - }); - //image href new window(emoji not included) - const images = view.find("img.raw[src]").removeClass("raw"); - images.each((key, value) => { + }) + }) + // gist + view.find('code[data-gist-id]').each((key, value) => { + if ($(value).children().length === 0) { $(value).gist(window.viewAjaxCallback) } + }) + // sequence diagram + const sequences = view.find('div.sequence-diagram.raw').removeClass('raw') + sequences.each((key, value) => { + try { + var $value = $(value) + const $ele = $(value).parent().parent() + + const sequence = $value + sequence.sequenceDiagram({ + theme: 'simple' + }) + + $ele.addClass('sequence-diagram') + $value.children().unwrap().unwrap() + 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() + $value.parent().append('<div class="alert alert-warning">' + err + '</div>') + console.warn(err) + } + }) + // flowchart + const flow = view.find('div.flow-chart.raw').removeClass('raw') + flow.each((key, value) => { + try { + var $value = $(value) + const $ele = $(value).parent().parent() + + const chart = window.flowchart.parse($value.text()) + $value.html('') + chart.drawSVG(value, { + 'line-width': 2, + 'fill': 'none', + 'font-size': '16px', + 'font-family': "'Andale Mono', monospace" + }) + + $ele.addClass('flow-chart') + $value.children().unwrap().unwrap() + } catch (err) { + $value.unwrap() + $value.parent().append('<div class="alert alert-warning">' + err + '</div>') + console.warn(err) + } + }) + // graphviz + var graphvizs = view.find('div.graphviz.raw').removeClass('raw') + graphvizs.each(function (key, value) { + try { + 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) { + $value.unwrap() + $value.parent().append('<div class="alert alert-warning">' + err + '</div>') + console.warn(err) + } + }) + // mermaid + const mermaids = view.find('div.mermaid.raw').removeClass('raw') + mermaids.each((key, value) => { + try { + var $value = $(value) + const $ele = $(value).closest('pre') + + let mermaidError = null + window.mermaid.parseError = (err, hash) => { + mermaidError = err + } + + if (window.mermaidAPI.parse($value.text())) { + $ele.addClass('mermaid') + $ele.html($value.text()) + window.mermaid.init(undefined, $ele) + } else { + throw new Error(mermaidError) + } + } catch (err) { + $value.unwrap() + $value.parent().append('<div class="alert alert-warning">' + err + '</div>') + console.warn(err) + } + }) + // image href new window(emoji not included) + const images = view.find('img.raw[src]').removeClass('raw') + images.each((key, value) => { // if it's already wrapped by link, then ignore - const $value = $(value); - $value[0].onload = e => { - if(viewAjaxCallback) viewAjaxCallback(); - }; - }); - //blockquote - 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 - 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") + const $value = $(value) + $value[0].onload = e => { + if (window.viewAjaxCallback) window.viewAjaxCallback() + } + }) + // blockquote + const blockquote = view.find('blockquote.raw').removeClass('raw') + const blockquoteP = blockquote.find('p') + blockquoteP.each((key, value) => { + let html = $(value).html() + html = replaceExtraTags(html) + $(value).html(html) + }) + // color tag in blockquote will change its left border color + const blockquoteColor = blockquote.find('.color') + blockquoteColor.each((key, value) => { + $(value).closest('blockquote').css('border-left-color', $(value).attr('data-color')) + }) + // slideshare + view.find('div.slideshare.raw').removeClass('raw') .each((key, value) => { - $.ajax({ - type: 'GET', - url: `//www.slideshare.net/api/oembed/2?url=http://www.slideshare.net/${$(value).attr('data-slideshareid')}&format=json`, - jsonp: 'callback', - dataType: 'jsonp', - 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(); - } - }); - }); - //speakerdeck - view.find("div.speakerdeck.raw").removeClass("raw") + $.ajax({ + type: 'GET', + url: `//www.slideshare.net/api/oembed/2?url=http://www.slideshare.net/${$(value).attr('data-slideshareid')}&format=json`, + jsonp: 'callback', + dataType: 'jsonp', + 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 (window.viewAjaxCallback) window.viewAjaxCallback() + } + }) + }) + // speakerdeck + view.find('div.speakerdeck.raw').removeClass('raw') .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}'`, - format: "json" - }, - dataType: "jsonp", - success(data) { - if (!data.query || !data.query.results) return; - const json = data.query.results.json; - const html = json.html; - var ratio = json.height / json.width; - $(value).html(html); - const iframe = $(value).children('iframe'); - const src = iframe.attr('src'); - if (src.indexOf('//') == 0) - 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}%`); - $(value).html(inner); - if(viewAjaxCallback) viewAjaxCallback(); - } - }); - }); - //pdf - view.find("div.pdf.raw").removeClass("raw") + 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}'`, + format: 'json' + }, + dataType: 'jsonp', + success (data) { + if (!data.query || !data.query.results) return + const json = data.query.results.json + const html = json.html + var ratio = json.height / json.width + $(value).html(html) + const iframe = $(value).children('iframe') + const src = iframe.attr('src') + if (src.indexOf('//') === 0) { iframe.attr('src', `https:${src}`) } + const inner = $('<div class="inner"></div>').append(iframe) + const height = iframe.attr('height') + const width = iframe.attr('width') + ratio = (height / width) * 100 + inner.css('padding-bottom', `${ratio}%`) + $(value).html(inner) + if (window.viewAjaxCallback) window.viewAjaxCallback() + } + }) + }) + // pdf + view.find('div.pdf.raw').removeClass('raw') .each(function (key, value) { - const url = $(value).attr('data-pdfurl'); - const inner = $('<div></div>'); - $(this).append(inner); - PDFObject.embed(url, inner, { - height: '400px' - }); - }); - //syntax highlighting - view.find("code.raw").removeClass("raw") + const url = $(value).attr('data-pdfurl') + const inner = $('<div></div>') + $(this).append(inner) + PDFObject.embed(url, inner, { + height: '400px' + }) + }) + // syntax highlighting + view.find('code.raw').removeClass('raw') .each((key, value) => { - const langDiv = $(value); - if (langDiv.length > 0) { - 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) { - var result = { - value: code - }; - } else if (reallang == "haskell" || reallang == "go" || reallang == "typescript" || reallang == "jsx") { - code = S(code).unescapeHTML().s; - var result = { - value: Prism.highlight(code, Prism.languages[reallang]) - }; - } else if (reallang == "tiddlywiki" || reallang == "mediawiki") { - code = S(code).unescapeHTML().s; - var result = { - value: Prism.highlight(code, Prism.languages.wiki) - }; - } else { - code = S(code).unescapeHTML().s; - const languages = hljs.listLanguages(); - if (!languages.includes(reallang)) { - var result = hljs.highlightAuto(code); - } else { - var result = hljs.highlight(reallang, code); - } - } - if (codeDiv.length > 0) codeDiv.html(result.value); - else langDiv.html(result.value); + const langDiv = $(value) + if (langDiv.length > 0) { + 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() + var result + if (!reallang) { + result = { + value: code + } + } else if (reallang === 'haskell' || reallang === 'go' || reallang === 'typescript' || reallang === 'jsx') { + code = S(code).unescapeHTML().s + result = { + value: Prism.highlight(code, Prism.languages[reallang]) + } + } else if (reallang === 'tiddlywiki' || reallang === 'mediawiki') { + code = S(code).unescapeHTML().s + result = { + value: Prism.highlight(code, Prism.languages.wiki) + } + } else { + code = S(code).unescapeHTML().s + const languages = hljs.listLanguages() + if (!languages.includes(reallang)) { + result = hljs.highlightAuto(code) + } else { + result = hljs.highlight(reallang, code) + } } - }); - //mathjax - const mathjaxdivs = view.find('span.mathjax.raw').removeClass("raw").toArray(); - try { - if (mathjaxdivs.length > 1) { - MathJax.Hub.Queue(["Typeset", MathJax.Hub, mathjaxdivs]); - MathJax.Hub.Queue(viewAjaxCallback); - } else if (mathjaxdivs.length > 0) { - MathJax.Hub.Queue(["Typeset", MathJax.Hub, mathjaxdivs[0]]); - MathJax.Hub.Queue(viewAjaxCallback); - } - } catch (err) { - console.warn(err); + if (codeDiv.length > 0) codeDiv.html(result.value) + else langDiv.html(result.value) + } + }) + // mathjax + const mathjaxdivs = view.find('span.mathjax.raw').removeClass('raw').toArray() + try { + if (mathjaxdivs.length > 1) { + window.MathJax.Hub.Queue(['Typeset', window.MathJax.Hub, mathjaxdivs]) + window.MathJax.Hub.Queue(window.viewAjaxCallback) + } else if (mathjaxdivs.length > 0) { + window.MathJax.Hub.Queue(['Typeset', window.MathJax.Hub, mathjaxdivs[0]]) + window.MathJax.Hub.Queue(window.viewAjaxCallback) } - //render title - document.title = renderTitle(view); + } catch (err) { + console.warn(err) + } + // render title + document.title = renderTitle(view) } -//only static transform should be here -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 - const linenumberdivs = result.find('.gutter.linenumber').toArray(); - for (let i = 0; i < linenumberdivs.length; i++) { - if ($(linenumberdivs[i]).hasClass('continue')) { - 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); - }); - } - } - // show yaml meta paring error - if (md.metaError) { - var warning = result.find('div#meta-error'); - if (warning && warning.length > 0) { - warning.text(md.metaError) - } else { - warning = $('<div id="meta-error" class="alert alert-warning">' + md.metaError + '</div>') - result.prepend(warning); - } +// only static transform should be here +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 + const linenumberdivs = result.find('.gutter.linenumber').toArray() + for (let i = 0; i < linenumberdivs.length; i++) { + if ($(linenumberdivs[i]).hasClass('continue')) { + 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) + }) + } + } + // show yaml meta paring error + if (md.metaError) { + var warning = result.find('div#meta-error') + if (warning && warning.length > 0) { + warning.text(md.metaError) + } else { + warning = $('<div id="meta-error" class="alert alert-warning">' + md.metaError + '</div>') + result.prepend(warning) } - return result; + } + return result } -window.postProcess = postProcess; - -function generateCleanHTML(view) { - const src = view.clone(); - const eles = src.find('*'); - //remove syncscroll parts - eles.removeClass('part'); - src.find('*[class=""]').removeAttr('class'); - eles.removeAttr('data-startline data-endline'); - src.find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll'); - //remove gist content - src.find("code[data-gist-id]").children().remove(); - //disable todo list - src.find("input.task-list-item-checkbox").attr('disabled', ''); - //replace emoji image path - 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`); - }); - //replace video to iframe - 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) { - const iframe = $('<iframe frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>'); - iframe.attr('src', url + id); - iframe.attr('style', style); - $(value).html(iframe); - } - }); - return src; +window.postProcess = postProcess + +function generateCleanHTML (view) { + const src = view.clone() + const eles = src.find('*') + // remove syncscroll parts + eles.removeClass('part') + src.find('*[class=""]').removeAttr('class') + eles.removeAttr('data-startline data-endline') + src.find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll') + // remove gist content + src.find('code[data-gist-id]').children().remove() + // disable todo list + src.find('input.task-list-item-checkbox').attr('disabled', '') + // replace emoji image path + 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`) + }) + // replace video to iframe + 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) { + const iframe = $('<iframe frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>') + iframe.attr('src', url + id) + iframe.attr('style', style) + $(value).html(iframe) + } + }) + return src } -export function exportToRawHTML(view) { - const filename = `${renderFilename(ui.area.markdown)}.html`; - const src = generateCleanHTML(view); - $(src).find('a.anchor').remove(); - const html = src[0].outerHTML; - const blob = new Blob([html], { - type: "text/html;charset=utf-8" - }); - saveAs(blob, filename, true); +export function exportToRawHTML (view) { + const filename = `${renderFilename(window.ui.area.markdown)}.html` + const src = generateCleanHTML(view) + $(src).find('a.anchor').remove() + const html = src[0].outerHTML + const blob = new Blob([html], { + type: 'text/html;charset=utf-8' + }) + saveAs(blob, filename, true) } -//extract markdown body to html and compile to template -export function exportToHTML(view) { - const title = renderTitle(ui.area.markdown); - const filename = `${renderFilename(ui.area.markdown)}.html`; - const src = generateCleanHTML(view); - //generate toc - const toc = $('#ui-toc').clone(); - toc.find('*').removeClass('active').find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll'); - 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`, css => { - $.get(`${serverurl}/views/html.hbs`, data => { - const template = Handlebars.compile(data); - const context = { - url: serverurl, - 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 - }; - const html = template(context); +// extract markdown body to html and compile to template +export function exportToHTML (view) { + const title = renderTitle(window.ui.area.markdown) + const filename = `${renderFilename(window.ui.area.markdown)}.html` + const src = generateCleanHTML(view) + // generate toc + const toc = $('#ui-toc').clone() + toc.find('*').removeClass('active').find("a[href^='#'][smoothhashscroll]").removeAttr('smoothhashscroll') + 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`, css => { + $.get(`${serverurl}/views/html.hbs`, data => { + const template = window.Handlebars.compile(data) + const context = { + url: serverurl, + 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 + } + const html = template(context) // console.log(html); - const blob = new Blob([html], { - type: "text/html;charset=utf-8" - }); - saveAs(blob, filename, true); - }); - }); + const blob = new Blob([html], { + type: 'text/html;charset=utf-8' + }) + saveAs(blob, filename, true) + }) + }) } -//jQuery sortByDepth +// jQuery sortByDepth $.fn.sortByDepth = function () { - const ar = this.map(function () { - return { - length: $(this).parents().length, - elt: this - } - }).get(); - - const result = []; - let i = ar.length; - ar.sort((a, b) => a.length - b.length); - while (i--) { - result.push(ar[i].elt); - } - return $(result); -}; - -function toggleTodoEvent(e) { - 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) { - let checked = null; - if (matches[1] == 'x') - checked = true; - else if (matches[1] == ' ') - checked = false; - const replacements = matches[0].match(/(^[>\s]*[\-\+\*]\s\[)([x ])(\])/); - editor.replaceRange(checked ? ' ' : 'x', { - line: startline, - ch: replacements[1].length - }, { - line: startline, - ch: replacements[1].length + 1 - }, '+input'); + const ar = this.map(function () { + return { + length: $(this).parents().length, + elt: this } + }).get() + + const result = [] + let i = ar.length + ar.sort((a, b) => a.length - b.length) + while (i--) { + result.push(ar[i].elt) + } + return $(result) } -//remove hash -function removeHash() { - history.pushState("", document.title, window.location.pathname + window.location.search); +function toggleTodoEvent (e) { + const startline = $(this).closest('li').attr('data-startline') - 1 + const line = window.editor.getLine(startline) + const matches = line.match(/^[>\s]*[-+*]\s\[([x ])\]/) + if (matches && matches.length >= 2) { + let checked = null + if (matches[1] === 'x') { checked = true } else if (matches[1] === ' ') { checked = false } + const replacements = matches[0].match(/(^[>\s]*[-+*]\s\[)([x ])(\])/) + window.editor.replaceRange(checked ? ' ' : 'x', { + line: startline, + ch: replacements[1].length + }, { + line: startline, + ch: replacements[1].length + 1 + }, '+input') + } } -let tocExpand = false; +// remove hash +function removeHash () { + history.pushState('', document.title, window.location.pathname + window.location.search) +} -function checkExpandToggle() { - const toc = $('.ui-toc-dropdown .toc'); - const toggle = $('.expand-toggle'); - if (!tocExpand) { - toc.removeClass('expand'); - toggle.text('Expand all'); - } else { - toc.addClass('expand'); - toggle.text('Collapse all'); - } +let tocExpand = false + +function checkExpandToggle () { + const toc = $('.ui-toc-dropdown .toc') + const toggle = $('.expand-toggle') + if (!tocExpand) { + toc.removeClass('expand') + toggle.text('Expand all') + } else { + toc.addClass('expand') + toggle.text('Collapse all') + } } -//toc -export function generateToc(id) { - const target = $(`#${id}`); - target.html(''); - new Toc('doc', { - 'level': 3, - 'top': -1, - 'class': 'toc', - 'ulClass': 'nav', - 'targetId': id, - 'process': getHeaderContent - }); - if (target.text() == 'undefined') - target.html(''); - 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(e => { - e.preventDefault(); - e.stopPropagation(); - tocExpand = !tocExpand; - checkExpandToggle(); - }); - backtotop.click(e => { - e.preventDefault(); - e.stopPropagation(); - if (scrollToTop) - scrollToTop(); - removeHash(); - }); - gotobottom.click(e => { - e.preventDefault(); - e.stopPropagation(); - if (scrollToBottom) - scrollToBottom(); - removeHash(); - }); - tocMenu.append(toggle).append(backtotop).append(gotobottom); - target.append(tocMenu); +// toc +export function generateToc (id) { + const target = $(`#${id}`) + target.html('') + /* eslint-disable no-unused-vars */ + var toc = new window.Toc('doc', { + 'level': 3, + 'top': -1, + 'class': 'toc', + 'ulClass': 'nav', + 'targetId': id, + 'process': getHeaderContent + }) + /* eslint-enable no-unsed-vars */ + if (target.text() === 'undefined') { target.html('') } + 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(e => { + e.preventDefault() + e.stopPropagation() + tocExpand = !tocExpand + checkExpandToggle() + }) + backtotop.click(e => { + e.preventDefault() + e.stopPropagation() + if (window.scrollToTop) { window.scrollToTop() } + removeHash() + }) + gotobottom.click(e => { + e.preventDefault() + e.stopPropagation() + if (window.scrollToBottom) { window.scrollToBottom() } + removeHash() + }) + tocMenu.append(toggle).append(backtotop).append(gotobottom) + target.append(tocMenu) } -//smooth all hash trigger scrolling -export function smoothHashScroll() { - const hashElements = $("a[href^='#']:not([smoothhashscroll])").toArray(); +// smooth all hash trigger scrolling +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) { + for (const element of hashElements) { + const $element = $(element) + const hash = element.hash + if (hash) { + $element.on('click', function (e) { // store hash - const hash = decodeURIComponent(this.hash); + const hash = decodeURIComponent(this.hash) // escape special characters in jquery selector - const $hash = $(hash.replace(/(:|\.|\[|\]|,)/g, "\\$1")); + const $hash = $(hash.replace(/(:|\.|\[|\]|,)/g, '\\$1')) // return if no element been selected - if ($hash.length <= 0) return; + if ($hash.length <= 0) return // prevent default anchor click behavior - e.preventDefault(); + e.preventDefault() // animate - $('body, html').stop(true, true).animate({ - scrollTop: $hash.offset().top - }, 100, "linear", () => { + $('body, html').stop(true, true).animate({ + scrollTop: $hash.offset().top + }, 100, 'linear', () => { // when done, add hash to url // (default click behaviour) - window.location.hash = hash; - }); - }); - $element.attr('smoothhashscroll', ''); - } + window.location.hash = hash + }) + }) + $element.attr('smoothhashscroll', '') } + } } -function imgPlayiframe(element, src) { - if (!$(element).attr("data-videoid")) return; - 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); +function imgPlayiframe (element, src) { + if (!$(element).attr('data-videoid')) return + 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) } const anchorForId = id => { - const anchor = document.createElement("a"); - anchor.className = "anchor hidden-xs"; - anchor.href = `#${id}`; - anchor.innerHTML = "<span class=\"octicon octicon-link\"></span>"; - anchor.title = id; - return anchor; -}; + const anchor = document.createElement('a') + anchor.className = 'anchor hidden-xs' + anchor.href = `#${id}` + anchor.innerHTML = '<span class="octicon octicon-link"></span>' + anchor.title = id + return anchor +} 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 - const id = slugifyWithUTF8(getHeaderContent(header)); - header.id = id; - } - header.insertBefore(anchorForId(header.id), header.firstChild); - } + 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 + const id = slugifyWithUTF8(getHeaderContent(header)) + header.id = id + } + header.insertBefore(anchorForId(header.id), header.firstChild) } -}; + } +} -export function autoLinkify(view) { - const contentBlock = view[0]; - if (!contentBlock) { - return; - } - for (let level = 1; level <= 6; level++) { - linkifyAnchors(level, contentBlock); - } +export function autoLinkify (view) { + const contentBlock = view[0] + if (!contentBlock) { + return + } + for (let level = 1; level <= 6; level++) { + linkifyAnchors(level, contentBlock) + } } -function getHeaderContent(header) { - const headerHTML = $(header).clone(); - headerHTML.find('.MathJax_Preview').remove(); - headerHTML.find('.MathJax').remove(); - return headerHTML[0].innerHTML; +function getHeaderContent (header) { + const headerHTML = $(header).clone() + headerHTML.find('.MathJax_Preview').remove() + headerHTML.find('.MathJax').remove() + return headerHTML[0].innerHTML } -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; - const duplicatedHeaders = view.find(`:header[id="${id}"]`).toArray(); - for (let j = 0; j < duplicatedHeaders.length; j++) { - if (duplicatedHeaders[j] != headers[i]) { - const newId = id + j; - const $duplicatedHeader = $(duplicatedHeaders[j]); - $duplicatedHeader.attr('id', newId); - const $headerLink = $duplicatedHeader.find(`> a.anchor[href="#${id}"]`); - $headerLink.attr('href', `#${newId}`); - $headerLink.attr('title', newId); - } - } +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 + const duplicatedHeaders = view.find(`:header[id="${id}"]`).toArray() + for (let j = 0; j < duplicatedHeaders.length; j++) { + if (duplicatedHeaders[j] !== headers[i]) { + const newId = id + j + const $duplicatedHeader = $(duplicatedHeaders[j]) + $duplicatedHeader.attr('id', newId) + const $headerLink = $duplicatedHeader.find(`> a.anchor[href="#${id}"]`) + $headerLink.attr('href', `#${newId}`) + $headerLink.attr('title', newId) + } } + } } -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); - const target = $(`#${id}`); - target.html(''); - new Toc('doc', { - 'level': 3, - 'top': -1, - 'class': 'toc', - 'targetId': id, - 'process': getHeaderContent - }); - if (target.text() == 'undefined') - target.html(''); - target.replaceWith(target.html()); - } +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) + const target = $(`#${id}`) + target.html('') + /* eslint-disable no-unused-vars */ + var toc = new window.Toc('doc', { + 'level': 3, + 'top': -1, + 'class': 'toc', + 'targetId': id, + 'process': getHeaderContent + }) + /* eslint-enable no-unused-vars */ + if (target.text() === 'undefined') { target.html('') } + target.replaceWith(target.html()) + } } -export function scrollToHash() { - const hash = location.hash; - location.hash = ""; - location.hash = hash; +export function scrollToHash () { + const hash = location.hash + location.hash = '' + location.hash = hash } -function highlightRender(code, lang) { - if (!lang || /no(-?)highlight|plain|text/.test(lang)) - return; - code = S(code).escapeHTML().s - if (lang == 'sequence') { - return `<div class="sequence-diagram raw">${code}</div>`; - } else if (lang == 'flow') { - return `<div class="flow-chart raw">${code}</div>`; - } else if (lang == 'graphviz') { - return `<div class="graphviz raw">${code}</div>`; - } else if (lang == 'mermaid') { - return `<div class="mermaid raw">${code}</div>`; +function highlightRender (code, lang) { + if (!lang || /no(-?)highlight|plain|text/.test(lang)) { return } + code = S(code).escapeHTML().s + if (lang === 'sequence') { + return `<div class="sequence-diagram raw">${code}</div>` + } else if (lang === 'flow') { + return `<div class="flow-chart raw">${code}</div>` + } else if (lang === 'graphviz') { + return `<div class="graphviz raw">${code}</div>` + } else if (lang === 'mermaid') { + return `<div class="mermaid raw">${code}</div>` + } + const result = { + value: code + } + const showlinenumbers = /=$|=\d+$|=\+$/.test(lang) + if (showlinenumbers) { + let startnumber = 1 + const matches = lang.match(/=(\d+)$/) + if (matches) { startnumber = parseInt(matches[1]) } + 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>` } - const result = { - value: code - }; - const showlinenumbers = /\=$|\=\d+$|\=\+$/.test(lang); - if (showlinenumbers) { - let startnumber = 1; - const matches = lang.match(/\=(\d+)$/); - if (matches) - startnumber = parseInt(matches[1]); - 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>`; - } - 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; + 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 } -import markdownit from 'markdown-it'; -import markdownitContainer from 'markdown-it-container'; +import markdownit from 'markdown-it' +import markdownitContainer from 'markdown-it-container' export let md = markdownit('default', { - html: true, - breaks: true, - langPrefix: "", - linkify: true, - typographer: true, - highlight: highlightRender -}); -window.md = md; - -md.use(require('markdown-it-abbr')); -md.use(require('markdown-it-footnote')); -md.use(require('markdown-it-deflist')); -md.use(require('markdown-it-mark')); -md.use(require('markdown-it-ins')); -md.use(require('markdown-it-sub')); -md.use(require('markdown-it-sup')); + html: true, + breaks: true, + langPrefix: '', + linkify: true, + typographer: true, + highlight: highlightRender +}) +window.md = md + +md.use(require('markdown-it-abbr')) +md.use(require('markdown-it-footnote')) +md.use(require('markdown-it-deflist')) +md.use(require('markdown-it-mark')) +md.use(require('markdown-it-ins')) +md.use(require('markdown-it-sub')) +md.use(require('markdown-it-sup')) md.use(require('markdown-it-mathjax')({ - beforeMath: '<span class="mathjax raw">', - afterMath: '</span>', - beforeInlineMath: '<span class="mathjax raw">\\(', - afterInlineMath: '\\)</span>', - beforeDisplayMath: '<span class="mathjax raw">\\[', - afterDisplayMath: '\\]</span>' -})); -md.use(require('markdown-it-imsize')); + beforeMath: '<span class="mathjax raw">', + afterMath: '</span>', + beforeInlineMath: '<span class="mathjax raw">\\(', + afterInlineMath: '\\)</span>', + beforeDisplayMath: '<span class="mathjax raw">\\[', + afterDisplayMath: '\\]</span>' +})) +md.use(require('markdown-it-imsize')) md.use(require('markdown-it-emoji'), { - shortcuts: {} -}); - -emojify.setConfig({ - blacklist: { - elements: ['script', 'textarea', 'a', 'pre', 'code', 'svg'], - classes: ['no-emojify'] - }, - img_dir: `${serverurl}/build/emojify.js/dist/images/basic`, - ignore_emoticons: true -}); - -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(...arguments); + shortcuts: {} +}) + +window.emojify.setConfig({ + blacklist: { + elements: ['script', 'textarea', 'a', 'pre', 'code', 'svg'], + classes: ['no-emojify'] + }, + img_dir: `${serverurl}/build/emojify.js/dist/images/basic`, + ignore_emoticons: true +}) + +md.renderer.rules.emoji = (token, idx) => window.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(...arguments) } -md.use(markdownitContainer, 'success', { render: renderContainer }); -md.use(markdownitContainer, 'info', { render: renderContainer }); -md.use(markdownitContainer, 'warning', { render: renderContainer }); -md.use(markdownitContainer, 'danger', { render: renderContainer }); +md.use(markdownitContainer, 'success', { render: renderContainer }) +md.use(markdownitContainer, 'info', { render: renderContainer }) +md.use(markdownitContainer, 'warning', { render: renderContainer }) +md.use(markdownitContainer, 'danger', { render: renderContainer }) md.renderer.rules.image = function (tokens, idx, options, env, self) { - tokens[idx].attrJoin('class', 'raw'); - return self.renderToken(...arguments); -}; + tokens[idx].attrJoin('class', 'raw') + return self.renderToken(...arguments) +} md.renderer.rules.list_item_open = function (tokens, idx, options, env, self) { - tokens[idx].attrJoin('class', 'raw'); - return self.renderToken(...arguments); -}; + tokens[idx].attrJoin('class', 'raw') + return self.renderToken(...arguments) +} md.renderer.rules.blockquote_open = function (tokens, idx, options, env, self) { - tokens[idx].attrJoin('class', 'raw'); - return self.renderToken(...arguments); -}; + tokens[idx].attrJoin('class', 'raw') + return self.renderToken(...arguments) +} md.renderer.rules.heading_open = function (tokens, idx, options, env, self) { - tokens[idx].attrJoin('class', 'raw'); - return self.renderToken(...arguments); -}; + tokens[idx].attrJoin('class', 'raw') + return self.renderToken(...arguments) +} 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]; - if (/\!$/.test(info)) token.attrJoin('class', 'wrap'); - token.attrJoin('class', options.langPrefix + langName.replace(/\=$|\=\d+$|\=\+$|\!$|\=\!$/, '')); - token.attrJoin('class', 'hljs'); - token.attrJoin('class', 'raw'); - } - - if (options.highlight) { - highlighted = options.highlight(token.content, langName) || md.utils.escapeHtml(token.content); - } else { - highlighted = md.utils.escapeHtml(token.content); - } - - if (highlighted.indexOf('<pre') === 0) { - return `${highlighted}\n`; - } - - return `<pre><code${self.renderAttrs(token)}>${highlighted}</code></pre>\n`; -}; + 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] + if (/!$/.test(info)) token.attrJoin('class', 'wrap') + token.attrJoin('class', options.langPrefix + langName.replace(/=$|=\d+$|=\+$|!$|=!$/, '')) + token.attrJoin('class', 'hljs') + token.attrJoin('class', 'raw') + } + + if (options.highlight) { + highlighted = options.highlight(token.content, langName) || md.utils.escapeHtml(token.content) + } else { + highlighted = md.utils.escapeHtml(token.content) + } + + if (highlighted.indexOf('<pre') === 0) { + return `${highlighted}\n` + } + + return `<pre><code${self.renderAttrs(token)}>${highlighted}</code></pre>\n` +} /* Defined regex markdown it plugins */ -import Plugin from 'markdown-it-regexp'; +import Plugin from 'markdown-it-regexp' -//youtube +// youtube const youtubePlugin = new Plugin( // regexp to match /{%youtube\s*([\d\D]*?)\s*%}/, (match, utils) => { - const videoid = match[1]; - if (!videoid) return; - const div = $('<div class="youtube raw"></div>'); - div.attr('data-videoid', videoid); - const thumbnail_src = `//img.youtube.com/vi/${videoid}/hqdefault.jpg`; - const image = `<img src="${thumbnail_src}" />`; - div.append(image); - const icon = '<i class="icon fa fa-youtube-play fa-5x"></i>'; - div.append(icon); - return div[0].outerHTML; + const videoid = match[1] + if (!videoid) return + const div = $('<div class="youtube raw"></div>') + div.attr('data-videoid', videoid) + const thumbnailSrc = `//img.youtube.com/vi/${videoid}/hqdefault.jpg` + const image = `<img src="${thumbnailSrc}" />` + div.append(image) + const icon = '<i class="icon fa fa-youtube-play fa-5x"></i>' + div.append(icon) + return div[0].outerHTML } -); -//vimeo +) +// vimeo const vimeoPlugin = new Plugin( // regexp to match /{%vimeo\s*([\d\D]*?)\s*%}/, (match, utils) => { - const videoid = match[1]; - if (!videoid) return; - const div = $('<div class="vimeo raw"></div>'); - div.attr('data-videoid', videoid); - const icon = '<i class="icon fa fa-vimeo-square fa-5x"></i>'; - div.append(icon); - return div[0].outerHTML; + const videoid = match[1] + if (!videoid) return + const div = $('<div class="vimeo raw"></div>') + div.attr('data-videoid', videoid) + const icon = '<i class="icon fa fa-vimeo-square fa-5x"></i>' + div.append(icon) + return div[0].outerHTML } -); -//gist +) +// gist const gistPlugin = new Plugin( // regexp to match /{%gist\s*([\d\D]*?)\s*%}/, (match, utils) => { - const gistid = match[1]; - const code = `<code data-gist-id="${gistid}"/>`; - return code; + const gistid = match[1] + const code = `<code data-gist-id="${gistid}"/>` + return code } -); -//TOC +) +// TOC const tocPlugin = new Plugin( // regexp to match /^\[TOC\]$/i, (match, utils) => '<div class="toc"></div>' -); -//slideshare +) +// slideshare const slidesharePlugin = new Plugin( // regexp to match /{%slideshare\s*([\d\D]*?)\s*%}/, (match, utils) => { - const slideshareid = match[1]; - const div = $('<div class="slideshare raw"></div>'); - div.attr('data-slideshareid', slideshareid); - return div[0].outerHTML; + const slideshareid = match[1] + const div = $('<div class="slideshare raw"></div>') + div.attr('data-slideshareid', slideshareid) + return div[0].outerHTML } -); -//speakerdeck +) +// speakerdeck const speakerdeckPlugin = new Plugin( // regexp to match /{%speakerdeck\s*([\d\D]*?)\s*%}/, (match, utils) => { - const speakerdeckid = match[1]; - const div = $('<div class="speakerdeck raw"></div>'); - div.attr('data-speakerdeckid', speakerdeckid); - return div[0].outerHTML; + const speakerdeckid = match[1] + const div = $('<div class="speakerdeck raw"></div>') + div.attr('data-speakerdeckid', speakerdeckid) + return div[0].outerHTML } -); -//pdf +) +// pdf const pdfPlugin = new Plugin( // regexp to match /{%pdf\s*([\d\D]*?)\s*%}/, (match, utils) => { - const pdfurl = match[1]; - if (!isValidURL(pdfurl)) return match[0]; - const div = $('<div class="pdf raw"></div>'); - div.attr('data-pdfurl', pdfurl); - return div[0].outerHTML; + const pdfurl = match[1] + if (!isValidURL(pdfurl)) return match[0] + const div = $('<div class="pdf raw"></div>') + div.attr('data-pdfurl', pdfurl) + return div[0].outerHTML } -); +) -//yaml meta, from https://github.com/eugeneware/remarkable-meta -function get(state, line) { - const pos = state.bMarks[line]; - const max = state.eMarks[line]; - return state.src.substr(pos, max - pos); +// yaml meta, from https://github.com/eugeneware/remarkable-meta +function get (state, line) { + const pos = state.bMarks[line] + const max = state.eMarks[line] + return state.src.substr(pos, max - pos) } -function meta(state, start, end, silent) { - if (start !== 0 || state.blkIndent !== 0) return false; - if (state.tShift[start] < 0) return false; - if (!get(state, start).match(/^---$/)) return false; - - const data = []; - for (var line = start + 1; line < end; line++) { - const str = get(state, line); - if (str.match(/^(\.{3}|-{3})$/)) break; - if (state.tShift[line] < 0) break; - data.push(str); - } - - if (line >= end) return false; - - try { - md.meta = jsyaml.safeLoad(data.join('\n')) || {}; - delete md.metaError; - } catch(err) { - md.metaError = err; - console.warn(err); - return false; - } - - state.line = line + 1; - - return true; +function meta (state, start, end, silent) { + if (start !== 0 || state.blkIndent !== 0) return false + if (state.tShift[start] < 0) return false + if (!get(state, start).match(/^---$/)) return false + + const data = [] + for (var line = start + 1; line < end; line++) { + const str = get(state, line) + if (str.match(/^(\.{3}|-{3})$/)) break + if (state.tShift[line] < 0) break + data.push(str) + } + + if (line >= end) return false + + try { + md.meta = window.jsyaml.safeLoad(data.join('\n')) || {} + delete md.metaError + } catch (err) { + md.metaError = err + console.warn(err) + return false + } + + state.line = line + 1 + + return true } -function metaPlugin(md) { - md.meta = md.meta || {}; - md.block.ruler.before('code', 'meta', meta, { - alt: [] - }); +function metaPlugin (md) { + md.meta = md.meta || {} + md.block.ruler.before('code', 'meta', meta, { + alt: [] + }) } -md.use(metaPlugin); -md.use(youtubePlugin); -md.use(vimeoPlugin); -md.use(gistPlugin); -md.use(tocPlugin); -md.use(slidesharePlugin); -md.use(speakerdeckPlugin); -md.use(pdfPlugin); +md.use(metaPlugin) +md.use(youtubePlugin) +md.use(vimeoPlugin) +md.use(gistPlugin) +md.use(tocPlugin) +md.use(slidesharePlugin) +md.use(speakerdeckPlugin) +md.use(pdfPlugin) export default { md -}; +} diff --git a/public/js/google-drive-picker.js b/public/js/google-drive-picker.js index 94aa77ff..5006cd25 100644 --- a/public/js/google-drive-picker.js +++ b/public/js/google-drive-picker.js @@ -1,119 +1,118 @@ -/**! +/** ! * Google Drive File Picker Example * By Daniel Lo Nigro (http://dan.cx/) */ -(function() { - /** - * Initialise a Google Driver file picker - */ - var FilePicker = window.FilePicker = function(options) { - // Config - this.apiKey = options.apiKey; - this.clientId = options.clientId; - - // Elements - this.buttonEl = options.buttonEl; - - // Events - this.onSelect = options.onSelect; - this.buttonEl.on('click', this.open.bind(this)); - - // Disable the button until the API loads, as it won't work properly until then. - this.buttonEl.prop('disabled', true); +(function () { + /** + * Initialise a Google Driver file picker + */ + var FilePicker = window.FilePicker = function (options) { + // Config + this.apiKey = options.apiKey + this.clientId = options.clientId - // Load the drive API - gapi.client.setApiKey(this.apiKey); - gapi.client.load('drive', 'v2', this._driveApiLoaded.bind(this)); - google.load('picker', '1', { callback: this._pickerApiLoaded.bind(this) }); - } + // Elements + this.buttonEl = options.buttonEl - FilePicker.prototype = { - /** - * Open the file picker. - */ - open: function() { - // Check if the user has already authenticated - var token = gapi.auth.getToken(); - if (token) { - this._showPicker(); - } else { - // The user has not yet authenticated with Google - // We need to do the authentication before displaying the Drive picker. - this._doAuth(false, function() { this._showPicker(); }.bind(this)); - } - }, - - /** - * Show the file picker once authentication has been done. - * @private - */ - _showPicker: function() { - var accessToken = gapi.auth.getToken().access_token; - 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). - setAppId(this.clientId). - setOAuthToken(accessToken). - setCallback(this._pickerCallback.bind(this)). - build(). - setVisible(true); - }, - - /** - * Called when a file has been selected in the Google Drive file picker. - * @private - */ - _pickerCallback: function(data) { - if (data[google.picker.Response.ACTION] == google.picker.Action.PICKED) { - var file = data[google.picker.Response.DOCUMENTS][0], - id = file[google.picker.Document.ID], - request = gapi.client.drive.files.get({ - fileId: id - }); - - request.execute(this._fileGetCallback.bind(this)); - } - }, - /** - * Called when file details have been retrieved from Google Drive. - * @private - */ - _fileGetCallback: function(file) { - if (this.onSelect) { - this.onSelect(file); - } - }, - - /** - * Called when the Google Drive file picker API has finished loading. - * @private - */ - _pickerApiLoaded: function() { - this.buttonEl.prop('disabled', false); - }, - - /** - * Called when the Google Drive API has finished loading. - * @private - */ - _driveApiLoaded: function() { - this._doAuth(true); - }, - - /** - * Authenticate with Google Drive via the Google JavaScript API. - * @private - */ - _doAuth: function(immediate, callback) { - gapi.auth.authorize({ - client_id: this.clientId, - scope: 'https://www.googleapis.com/auth/drive.readonly', - immediate: immediate - }, callback ? callback : function() {}); - } - }; -}()); + // Events + this.onSelect = options.onSelect + this.buttonEl.on('click', this.open.bind(this)) + + // Disable the button until the API loads, as it won't work properly until then. + this.buttonEl.prop('disabled', true) + + // Load the drive API + window.gapi.client.setApiKey(this.apiKey) + window.gapi.client.load('drive', 'v2', this._driveApiLoaded.bind(this)) + window.google.load('picker', '1', { callback: this._pickerApiLoaded.bind(this) }) + } + + FilePicker.prototype = { + /** + * Open the file picker. + */ + open: function () { + // Check if the user has already authenticated + var token = window.gapi.auth.getToken() + if (token) { + this._showPicker() + } else { + // The user has not yet authenticated with Google + // We need to do the authentication before displaying the Drive picker. + this._doAuth(false, function () { this._showPicker() }.bind(this)) + } + }, + + /** + * Show the file picker once authentication has been done. + * @private + */ + _showPicker: function () { + var accessToken = window.gapi.auth.getToken().access_token + var view = new window.google.picker.DocsView() + view.setMimeTypes('text/markdown,text/html') + view.setIncludeFolders(true) + view.setOwnedByMe(true) + this.picker = new window.google.picker.PickerBuilder() + .enableFeature(window.google.picker.Feature.NAV_HIDDEN) + .addView(view) + .setAppId(this.clientId) + .setOAuthToken(accessToken) + .setCallback(this._pickerCallback.bind(this)) + .build() + .setVisible(true) + }, + + /** + * Called when a file has been selected in the Google Drive file picker. + * @private + */ + _pickerCallback: function (data) { + if (data[window.google.picker.Response.ACTION] === window.google.picker.Action.PICKED) { + var file = data[window.google.picker.Response.DOCUMENTS][0] + var id = file[window.google.picker.Document.ID] + var request = window.gapi.client.drive.files.get({ + fileId: id + }) + request.execute(this._fileGetCallback.bind(this)) + } + }, + /** + * Called when file details have been retrieved from Google Drive. + * @private + */ + _fileGetCallback: function (file) { + if (this.onSelect) { + this.onSelect(file) + } + }, + + /** + * Called when the Google Drive file picker API has finished loading. + * @private + */ + _pickerApiLoaded: function () { + this.buttonEl.prop('disabled', false) + }, + + /** + * Called when the Google Drive API has finished loading. + * @private + */ + _driveApiLoaded: function () { + this._doAuth(true) + }, + + /** + * Authenticate with Google Drive via the Google JavaScript API. + * @private + */ + _doAuth: function (immediate, callback) { + window.gapi.auth.authorize({ + client_id: this.clientId, + scope: 'https://www.googleapis.com/auth/drive.readonly', + immediate: immediate + }, callback || function () {}) + } + } +}()) diff --git a/public/js/google-drive-upload.js b/public/js/google-drive-upload.js index eabc5b7f..6c0e8a62 100644 --- a/public/js/google-drive-upload.js +++ b/public/js/google-drive-upload.js @@ -1,30 +1,31 @@ +/* eslint-env browser, jquery */ /** * Helper for implementing retries with backoff. Initial retry * delay is 1 second, increasing by 2x (+jitter) for subsequent retries * * @constructor */ -var RetryHandler = function() { - this.interval = 1000; // Start at one second - this.maxInterval = 60 * 1000; // Don't wait longer than a minute -}; +var RetryHandler = function () { + this.interval = 1000 // Start at one second + this.maxInterval = 60 * 1000 // Don't wait longer than a minute +} /** * Invoke the function after waiting * * @param {function} fn Function to invoke */ -RetryHandler.prototype.retry = function(fn) { - setTimeout(fn, this.interval); - this.interval = this.nextInterval_(); -}; +RetryHandler.prototype.retry = function (fn) { + setTimeout(fn, this.interval) + this.interval = this.nextInterval_() +} /** * Reset the counter (e.g. after successful request.) */ -RetryHandler.prototype.reset = function() { - this.interval = 1000; -}; +RetryHandler.prototype.reset = function () { + this.interval = 1000 +} /** * Calculate the next wait time. @@ -32,10 +33,10 @@ RetryHandler.prototype.reset = function() { * * @private */ -RetryHandler.prototype.nextInterval_ = function() { - var interval = this.interval * 2 + this.getRandomInt_(0, 1000); - return Math.min(interval, this.maxInterval); -}; +RetryHandler.prototype.nextInterval_ = function () { + var interval = this.interval * 2 + this.getRandomInt_(0, 1000) + return Math.min(interval, this.maxInterval) +} /** * Get a random int in the range of min to max. Used to add jitter to wait times. @@ -44,10 +45,9 @@ RetryHandler.prototype.nextInterval_ = function() { * @param {number} max Upper bounds * @private */ -RetryHandler.prototype.getRandomInt_ = function(min, max) { - return Math.floor(Math.random() * (max - min + 1) + min); -}; - +RetryHandler.prototype.getRandomInt_ = function (min, max) { + return Math.floor(Math.random() * (max - min + 1) + min) +} /** * Helper class for resumable uploads using XHR/CORS. Can upload any Blob-like item, whether @@ -75,116 +75,115 @@ RetryHandler.prototype.getRandomInt_ = function(min, max) { * @param {function} [options.onProgress] Callback for status for the in-progress upload * @param {function} [options.onError] Callback if upload fails */ -var MediaUploader = function(options) { - var noop = function() {}; - this.file = options.file; - this.contentType = options.contentType || this.file.type || 'application/octet-stream'; +var MediaUploader = function (options) { + var noop = function () {} + this.file = options.file + this.contentType = options.contentType || this.file.type || 'application/octet-stream' this.metadata = options.metadata || { 'title': this.file.name, 'mimeType': this.contentType - }; - this.token = options.token; - this.onComplete = options.onComplete || noop; - this.onProgress = options.onProgress || noop; - this.onError = options.onError || noop; - this.offset = options.offset || 0; - this.chunkSize = options.chunkSize || 0; - this.retryHandler = new RetryHandler(); + } + this.token = options.token + this.onComplete = options.onComplete || noop + this.onProgress = options.onProgress || noop + this.onError = options.onError || noop + this.offset = options.offset || 0 + this.chunkSize = options.chunkSize || 0 + this.retryHandler = new RetryHandler() - this.url = options.url; + this.url = options.url if (!this.url) { - var params = options.params || {}; - params.uploadType = 'resumable'; - this.url = this.buildUrl_(options.fileId, params, options.baseUrl); + var params = options.params || {} + params.uploadType = 'resumable' + this.url = this.buildUrl_(options.fileId, params, options.baseUrl) } - this.httpMethod = options.fileId ? 'PUT' : 'POST'; -}; + this.httpMethod = options.fileId ? 'PUT' : 'POST' +} /** * Initiate the upload. */ -MediaUploader.prototype.upload = function() { - var self = this; - var xhr = new XMLHttpRequest(); +MediaUploader.prototype.upload = function () { + var xhr = new XMLHttpRequest() - xhr.open(this.httpMethod, this.url, true); - xhr.setRequestHeader('Authorization', 'Bearer ' + this.token); - xhr.setRequestHeader('Content-Type', 'application/json'); - xhr.setRequestHeader('X-Upload-Content-Length', this.file.size); - xhr.setRequestHeader('X-Upload-Content-Type', this.contentType); + xhr.open(this.httpMethod, this.url, true) + xhr.setRequestHeader('Authorization', 'Bearer ' + this.token) + xhr.setRequestHeader('Content-Type', 'application/json') + xhr.setRequestHeader('X-Upload-Content-Length', this.file.size) + xhr.setRequestHeader('X-Upload-Content-Type', this.contentType) - xhr.onload = function(e) { + xhr.onload = function (e) { if (e.target.status < 400) { - var location = e.target.getResponseHeader('Location'); - this.url = location; - this.sendFile_(); + var location = e.target.getResponseHeader('Location') + this.url = location + this.sendFile_() } else { - this.onUploadError_(e); + this.onUploadError_(e) } - }.bind(this); - xhr.onerror = this.onUploadError_.bind(this); - xhr.send(JSON.stringify(this.metadata)); -}; + }.bind(this) + xhr.onerror = this.onUploadError_.bind(this) + xhr.send(JSON.stringify(this.metadata)) +} /** * Send the actual file content. * * @private */ -MediaUploader.prototype.sendFile_ = function() { - var content = this.file; - var end = this.file.size; +MediaUploader.prototype.sendFile_ = function () { + var content = this.file + var end = this.file.size if (this.offset || this.chunkSize) { // Only bother to slice the file if we're either resuming or uploading in chunks if (this.chunkSize) { - end = Math.min(this.offset + this.chunkSize, this.file.size); + end = Math.min(this.offset + this.chunkSize, this.file.size) } - content = content.slice(this.offset, end); + content = content.slice(this.offset, end) } - var xhr = new XMLHttpRequest(); - xhr.open('PUT', this.url, true); - xhr.setRequestHeader('Content-Type', this.contentType); - xhr.setRequestHeader('Content-Range', "bytes " + this.offset + "-" + (end - 1) + "/" + this.file.size); - xhr.setRequestHeader('X-Upload-Content-Type', this.file.type); + var xhr = new XMLHttpRequest() + xhr.open('PUT', this.url, true) + xhr.setRequestHeader('Content-Type', this.contentType) + xhr.setRequestHeader('Content-Range', 'bytes ' + this.offset + '-' + (end - 1) + '/' + this.file.size) + xhr.setRequestHeader('X-Upload-Content-Type', this.file.type) if (xhr.upload) { - xhr.upload.addEventListener('progress', this.onProgress); + xhr.upload.addEventListener('progress', this.onProgress) } - xhr.onload = this.onContentUploadSuccess_.bind(this); - xhr.onerror = this.onContentUploadError_.bind(this); - xhr.send(content); -}; + xhr.onload = this.onContentUploadSuccess_.bind(this) + xhr.onerror = this.onContentUploadError_.bind(this) + xhr.send(content) +} /** * Query for the state of the file for resumption. * * @private */ -MediaUploader.prototype.resume_ = function() { - var xhr = new XMLHttpRequest(); - xhr.open('PUT', this.url, true); - xhr.setRequestHeader('Content-Range', "bytes */" + this.file.size); - xhr.setRequestHeader('X-Upload-Content-Type', this.file.type); +MediaUploader.prototype.resume_ = function () { + var xhr = new XMLHttpRequest() + xhr.open('PUT', this.url, true) + xhr.setRequestHeader('Content-Range', 'bytes */' + this.file.size) + xhr.setRequestHeader('X-Upload-Content-Type', this.file.type) if (xhr.upload) { - xhr.upload.addEventListener('progress', this.onProgress); + xhr.upload.addEventListener('progress', this.onProgress) } - xhr.onload = this.onContentUploadSuccess_.bind(this); - xhr.onerror = this.onContentUploadError_.bind(this); - xhr.send(); -}; + xhr.onload = this.onContentUploadSuccess_.bind(this) + xhr.onerror = this.onContentUploadError_.bind(this) + xhr.send() +} /** * Extract the last saved range if available in the request. * * @param {XMLHttpRequest} xhr Request object */ -MediaUploader.prototype.extractRange_ = function(xhr) { - var range = xhr.getResponseHeader('Range'); +MediaUploader.prototype.extractRange_ = function (xhr) { + var range = xhr.getResponseHeader('Range') if (range) { - this.offset = parseInt(range.match(/\d+/g).pop(), 10) + 1; + this.offset = parseInt(range.match(/\d+/g).pop(), 10) + 1 } -}; +} /** * Handle successful responses for uploads. Depending on the context, @@ -194,17 +193,17 @@ MediaUploader.prototype.extractRange_ = function(xhr) { * @private * @param {object} e XHR event */ -MediaUploader.prototype.onContentUploadSuccess_ = function(e) { - if (e.target.status == 200 || e.target.status == 201) { - this.onComplete(e.target.response); - } else if (e.target.status == 308) { - this.extractRange_(e.target); - this.retryHandler.reset(); - this.sendFile_(); +MediaUploader.prototype.onContentUploadSuccess_ = function (e) { + if (e.target.status === 200 || e.target.status === 201) { + this.onComplete(e.target.response) + } else if (e.target.status === 308) { + this.extractRange_(e.target) + this.retryHandler.reset() + this.sendFile_() } else { - this.onContentUploadError_(e); + this.onContentUploadError_(e) } -}; +} /** * Handles errors for uploads. Either retries or aborts depending @@ -213,13 +212,13 @@ MediaUploader.prototype.onContentUploadSuccess_ = function(e) { * @private * @param {object} e XHR event */ -MediaUploader.prototype.onContentUploadError_ = function(e) { +MediaUploader.prototype.onContentUploadError_ = function (e) { if (e.target.status && e.target.status < 500) { - this.onError(e.target.response); + this.onError(e.target.response) } else { - this.retryHandler.retry(this.resume_.bind(this)); + this.retryHandler.retry(this.resume_.bind(this)) } -}; +} /** * Handles errors for the initial request. @@ -227,9 +226,9 @@ MediaUploader.prototype.onContentUploadError_ = function(e) { * @private * @param {object} e XHR event */ -MediaUploader.prototype.onUploadError_ = function(e) { - this.onError(e.target.response); // TODO - Retries for initial upload -}; +MediaUploader.prototype.onUploadError_ = function (e) { + this.onError(e.target.response) // TODO - Retries for initial upload +} /** * Construct a query string from a hash/object @@ -238,12 +237,12 @@ MediaUploader.prototype.onUploadError_ = function(e) { * @param {object} [params] Key/value pairs for query string * @return {string} query string */ -MediaUploader.prototype.buildQuery_ = function(params) { - params = params || {}; - return Object.keys(params).map(function(key) { - return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]); - }).join('&'); -}; +MediaUploader.prototype.buildQuery_ = function (params) { + params = params || {} + return Object.keys(params).map(function (key) { + return encodeURIComponent(key) + '=' + encodeURIComponent(params[key]) + }).join('&') +} /** * Build the drive upload URL @@ -253,16 +252,16 @@ MediaUploader.prototype.buildQuery_ = function(params) { * @param {object} [params] Query parameters * @return {string} URL */ -MediaUploader.prototype.buildUrl_ = function(id, params, baseUrl) { - var url = baseUrl || 'https://www.googleapis.com/upload/drive/v2/files/'; +MediaUploader.prototype.buildUrl_ = function (id, params, baseUrl) { + var url = baseUrl || 'https://www.googleapis.com/upload/drive/v2/files/' if (id) { - url += id; + url += id } - var query = this.buildQuery_(params); + var query = this.buildQuery_(params) if (query) { - url += '?' + query; + url += '?' + query } - return url; -}; + return url +} -window.MediaUploader = MediaUploader; +window.MediaUploader = MediaUploader diff --git a/public/js/history.js b/public/js/history.js index 34b2cba7..e14b80d8 100644 --- a/public/js/history.js +++ b/public/js/history.js @@ -1,372 +1,328 @@ -import store from 'store'; -import S from 'string'; +/* eslint-env browser, jquery */ +/* global serverurl, Cookies, moment */ + +import store from 'store' +import S from 'string' import { checkIfAuth -} from './lib/common/login'; +} from './lib/common/login' import { urlpath -} from './lib/config'; +} from './lib/config' -window.migrateHistoryFromTempCallback = null; +window.migrateHistoryFromTempCallback = null -migrateHistoryFromTemp(); +migrateHistoryFromTemp() -function migrateHistoryFromTemp() { - if (url('#tempid')) { - $.get(`${serverurl}/temp`, { - tempid: url('#tempid') - }) - .done(data => { - if (data && data.temp) { - getStorageHistory(olddata => { - if (!olddata || olddata.length == 0) { - saveHistoryToStorage(JSON.parse(data.temp)); - } - }); - } - }) - .always(() => { - let hash = location.hash.split('#')[1]; - hash = hash.split('&'); - for (let i = 0; i < hash.length; i++) - if (hash[i].indexOf('tempid') == 0) { - hash.splice(i, 1); - i--; - } - hash = hash.join('&'); - location.hash = hash; - if (migrateHistoryFromTempCallback) - migrateHistoryFromTempCallback(); - }); - } +function migrateHistoryFromTemp () { + if (window.url('#tempid')) { + $.get(`${serverurl}/temp`, { + tempid: window.url('#tempid') + }) + .done(data => { + if (data && data.temp) { + getStorageHistory(olddata => { + if (!olddata || olddata.length === 0) { + saveHistoryToStorage(JSON.parse(data.temp)) + } + }) + } + }) + .always(() => { + let hash = location.hash.split('#')[1] + hash = hash.split('&') + for (let i = 0; i < hash.length; i++) { + if (hash[i].indexOf('tempid') === 0) { + hash.splice(i, 1) + i-- + } + } + hash = hash.join('&') + location.hash = hash + if (window.migrateHistoryFromTempCallback) { window.migrateHistoryFromTempCallback() } + }) + } } -export function saveHistory(notehistory) { - checkIfAuth( +export function saveHistory (notehistory) { + checkIfAuth( () => { - saveHistoryToServer(notehistory); + saveHistoryToServer(notehistory) }, () => { - saveHistoryToStorage(notehistory); + saveHistoryToStorage(notehistory) } - ); + ) } -function saveHistoryToStorage(notehistory) { - if (store.enabled) - store.set('notehistory', JSON.stringify(notehistory)); - else - saveHistoryToCookie(notehistory); +function saveHistoryToStorage (notehistory) { + if (store.enabled) { store.set('notehistory', JSON.stringify(notehistory)) } else { saveHistoryToCookie(notehistory) } } -function saveHistoryToCookie(notehistory) { - Cookies.set('notehistory', notehistory, { - expires: 365 - }); +function saveHistoryToCookie (notehistory) { + Cookies.set('notehistory', notehistory, { + expires: 365 + }) } -function saveHistoryToServer(notehistory) { - $.post(`${serverurl}/history`, { - history: JSON.stringify(notehistory) - }); +function saveHistoryToServer (notehistory) { + $.post(`${serverurl}/history`, { + history: JSON.stringify(notehistory) + }) } -function saveCookieHistoryToStorage(callback) { - store.set('notehistory', Cookies.get('notehistory')); - callback(); -} - -export function saveStorageHistoryToServer(callback) { - const data = store.get('notehistory'); - if (data) { - $.post(`${serverurl}/history`, { - history: data - }) - .done(data => { - callback(data); - }); - } -} - -function saveCookieHistoryToServer(callback) { +export function saveStorageHistoryToServer (callback) { + const data = store.get('notehistory') + if (data) { $.post(`${serverurl}/history`, { - history: Cookies.get('notehistory') - }) - .done(data => { - callback(data); - }); + history: data + }) + .done(data => { + callback(data) + }) + } } -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) { - 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]; - } - found = true; - break; - } +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) { + 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] } - if (!found) - newnotehistory.push(notehistory[i]); + found = true + break + } } - return newnotehistory; + if (!found) { newnotehistory.push(notehistory[i]) } + } + return newnotehistory } -function addHistory(id, text, time, tags, pinned, notehistory) { +function addHistory (id, text, time, tags, pinned, notehistory) { // only add when note id exists - if (id) { - notehistory.push({ - id, - text, - time, - tags, - pinned - }); - } - return notehistory; + if (id) { + notehistory.push({ + id, + text, + time, + tags, + pinned + }) + } + return notehistory } -export function removeHistory(id, notehistory) { - for (let i = 0; i < notehistory.length; i++) { - if (notehistory[i].id == id) { - notehistory.splice(i, 1); - i -= 1; - } +export function removeHistory (id, notehistory) { + for (let i = 0; i < notehistory.length; i++) { + if (notehistory[i].id === id) { + notehistory.splice(i, 1) + i -= 1 } - return notehistory; + } + return notehistory } -//used for inner -export function writeHistory(title, tags) { - checkIfAuth( +// used for inner +export function writeHistory (title, tags) { + checkIfAuth( () => { // no need to do this anymore, this will count from server-side // writeHistoryToServer(title, tags); }, () => { - writeHistoryToStorage(title, tags); + writeHistoryToStorage(title, tags) } - ); + ) } -function writeHistoryToServer(title, tags) { - $.get(`${serverurl}/history`) - .done(data => { - try { - if (data.history) { - var notehistory = data.history; - } else { - var notehistory = []; - } - } catch (err) { - var notehistory = []; - } - if (!notehistory) - notehistory = []; - - const newnotehistory = generateHistory(title, tags, notehistory); - saveHistoryToServer(newnotehistory); - }) - .fail((xhr, status, error) => { - console.error(xhr.responseText); - }); +function writeHistoryToCookie (title, tags) { + var notehistory + try { + notehistory = Cookies.getJSON('notehistory') + } catch (err) { + notehistory = [] + } + if (!notehistory) { notehistory = [] } + const newnotehistory = generateHistory(title, tags, notehistory) + saveHistoryToCookie(newnotehistory) } -function writeHistoryToCookie(title, tags) { - try { - var notehistory = Cookies.getJSON('notehistory'); - } catch (err) { - var notehistory = []; - } - if (!notehistory) - notehistory = []; - - const newnotehistory = generateHistory(title, tags, notehistory); - saveHistoryToCookie(newnotehistory); -} - -function writeHistoryToStorage(title, tags) { - if (store.enabled) { - let data = store.get('notehistory'); - if (data) { - if (typeof data == "string") - data = JSON.parse(data); - var notehistory = data; - } else - var notehistory = []; - if (!notehistory) - notehistory = []; - - const newnotehistory = generateHistory(title, tags, notehistory); - saveHistoryToStorage(newnotehistory); +function writeHistoryToStorage (title, tags) { + if (store.enabled) { + let data = store.get('notehistory') + var notehistory + if (data) { + if (typeof data === 'string') { data = JSON.parse(data) } + notehistory = data } else { - writeHistoryToCookie(title, tags); + notehistory = [] } + if (!notehistory) { notehistory = [] } + + const newnotehistory = generateHistory(title, tags, notehistory) + saveHistoryToStorage(newnotehistory) + } else { + writeHistoryToCookie(title, tags) + } } if (!Array.isArray) { - Array.isArray = arg => Object.prototype.toString.call(arg) === '[object Array]'; + Array.isArray = arg => Object.prototype.toString.call(arg) === '[object Array]' } -function renderHistory(title, tags) { - //console.debug(tags); - const id = urlpath ? location.pathname.slice(urlpath.length + 1, location.pathname.length).split('/')[1] : location.pathname.split('/')[1]; - return { - id, - text: title, - time: moment().valueOf(), - tags - }; +function renderHistory (title, tags) { + // console.debug(tags); + const id = urlpath ? location.pathname.slice(urlpath.length + 1, location.pathname.length).split('/')[1] : location.pathname.split('/')[1] + return { + id, + text: title, + time: moment().valueOf(), + tags + } } -function generateHistory(title, tags, notehistory) { - 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; - } +function generateHistory (title, tags, notehistory) { + 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); - return notehistory; + } + notehistory = removeHistory(info.id, notehistory) + notehistory = addHistory(info.id, info.text, info.time, info.tags, pinned, notehistory) + notehistory = clearDuplicatedHistory(notehistory) + return notehistory } -//used for outer -export function getHistory(callback) { - checkIfAuth( +// used for outer +export function getHistory (callback) { + checkIfAuth( () => { - getServerHistory(callback); + getServerHistory(callback) }, () => { - getStorageHistory(callback); + getStorageHistory(callback) } - ); + ) } -function getServerHistory(callback) { - $.get(`${serverurl}/history`) +function getServerHistory (callback) { + $.get(`${serverurl}/history`) .done(data => { - if (data.history) { - callback(data.history); - } + if (data.history) { + callback(data.history) + } }) .fail((xhr, status, error) => { - console.error(xhr.responseText); - }); + console.error(xhr.responseText) + }) } -function getCookieHistory(callback) { - callback(Cookies.getJSON('notehistory')); +function getCookieHistory (callback) { + callback(Cookies.getJSON('notehistory')) } -export function getStorageHistory(callback) { - if (store.enabled) { - let data = store.get('notehistory'); - if (data) { - if (typeof data == "string") - data = JSON.parse(data); - callback(data); - } else - getCookieHistory(callback); - } else { - getCookieHistory(callback); - } +export function getStorageHistory (callback) { + if (store.enabled) { + let data = store.get('notehistory') + if (data) { + if (typeof data === 'string') { data = JSON.parse(data) } + callback(data) + } else { getCookieHistory(callback) } + } else { + getCookieHistory(callback) + } } -export function parseHistory(list, callback) { - checkIfAuth( +export function parseHistory (list, callback) { + checkIfAuth( () => { - parseServerToHistory(list, callback); + parseServerToHistory(list, callback) }, () => { - parseStorageToHistory(list, callback); + parseStorageToHistory(list, callback) } - ); + ) } -export function parseServerToHistory(list, callback) { - $.get(`${serverurl}/history`) +export function parseServerToHistory (list, callback) { + $.get(`${serverurl}/history`) .done(data => { - if (data.history) { - parseToHistory(list, data.history, callback); - } + if (data.history) { + parseToHistory(list, data.history, callback) + } }) .fail((xhr, status, error) => { - console.error(xhr.responseText); - }); + console.error(xhr.responseText) + }) } -function parseCookieToHistory(list, callback) { - const notehistory = Cookies.getJSON('notehistory'); - parseToHistory(list, notehistory, callback); +function parseCookieToHistory (list, callback) { + const notehistory = Cookies.getJSON('notehistory') + parseToHistory(list, notehistory, callback) } -export function parseStorageToHistory(list, callback) { - if (store.enabled) { - let data = store.get('notehistory'); - if (data) { - if (typeof data == "string") - data = JSON.parse(data); - parseToHistory(list, data, callback); - } else - parseCookieToHistory(list, callback); - } else { - parseCookieToHistory(list, callback); - } +export function parseStorageToHistory (list, callback) { + if (store.enabled) { + let data = store.get('notehistory') + if (data) { + if (typeof data === 'string') { data = JSON.parse(data) } + parseToHistory(list, data, callback) + } else { parseCookieToHistory(list, callback) } + } else { + parseCookieToHistory(list, callback) + } } -function parseToHistory(list, notehistory, callback) { - if (!callback) return; - else if (!list || !notehistory) callback(list, notehistory); - else if (notehistory && notehistory.length > 0) { - for (let i = 0; i < notehistory.length; i++) { - //parse time to timestamp and fromNow - 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'); +function parseToHistory (list, notehistory, callback) { + if (!callback) return + else if (!list || !notehistory) callback(list, notehistory) + else if (notehistory && notehistory.length > 0) { + for (let i = 0; i < notehistory.length; i++) { + // parse time to timestamp and fromNow + 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') // prevent XSS - notehistory[i].text = S(notehistory[i].text).escapeHTML().s; - notehistory[i].tags = (notehistory[i].tags && notehistory[i].tags.length > 0) ? S(notehistory[i].tags).escapeHTML().s.split(',') : []; + notehistory[i].text = S(notehistory[i].text).escapeHTML().s + notehistory[i].tags = (notehistory[i].tags && notehistory[i].tags.length > 0) ? S(notehistory[i].tags).escapeHTML().s.split(',') : [] // add to list - if (notehistory[i].id && list.get('id', notehistory[i].id).length == 0) - list.add(notehistory[i]); - } + if (notehistory[i].id && list.get('id', notehistory[i].id).length === 0) { list.add(notehistory[i]) } } - callback(list, notehistory); + } + callback(list, notehistory) } -export function postHistoryToServer(noteId, data, callback) { - $.post(`${serverurl}/history/${noteId}`, data) +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); - }); + console.error(xhr.responseText) + return callback(error, null) + }) } -export function deleteServerHistory(noteId, callback) { - $.ajax({ - url: `${serverurl}/history${noteId ? '/' + noteId : ""}`, - type: 'DELETE' - }) +export function deleteServerHistory (noteId, callback) { + $.ajax({ + url: `${serverurl}/history${noteId ? '/' + noteId : ''}`, + type: 'DELETE' + }) .done(result => callback(null, result)) .fail((xhr, status, error) => { - console.error(xhr.responseText); - return callback(error, null); - }); + console.error(xhr.responseText) + return callback(error, null) + }) } diff --git a/public/js/htmlExport.js b/public/js/htmlExport.js index 1c2c5eb9..1a873aca 100644 --- a/public/js/htmlExport.js +++ b/public/js/htmlExport.js @@ -1,6 +1,6 @@ -require('../css/github-extract.css'); -require('../css/markdown.css'); -require('../css/extra.css'); -require('../css/slide-preview.css'); -require('../css/google-font.css'); -require('../css/site.css'); +require('../css/github-extract.css') +require('../css/markdown.css') +require('../css/extra.css') +require('../css/slide-preview.css') +require('../css/google-font.css') +require('../css/site.css') diff --git a/public/js/index.js b/public/js/index.js index f0c476ef..e672a68d 100644 --- a/public/js/index.js +++ b/public/js/index.js @@ -1,26 +1,30 @@ -/* jquery and jquery plugins */ -require('../vendor/showup/showup'); +/* eslint-env browser, jquery */ +/* global CodeMirror, Cookies, moment, editor, ui, Spinner, + modeType, Idle, serverurl, key, gapi, Dropbox, FilePicker + ot, MediaUploader, hex2rgb, num_loaded, Visibility */ -require('../css/index.css'); -require('../css/extra.css'); -require('../css/slide-preview.css'); -require('../css/site.css'); +require('../vendor/showup/showup') -require('highlight.js/styles/github-gist.css'); +require('../css/index.css') +require('../css/extra.css') +require('../css/slide-preview.css') +require('../css/site.css') -var toMarkdown = require('to-markdown'); +require('highlight.js/styles/github-gist.css') -var saveAs = require('file-saver').saveAs; -var randomColor = require('randomcolor'); +var toMarkdown = require('to-markdown') -var _ = require("lodash"); +var saveAs = require('file-saver').saveAs +var randomColor = require('randomcolor') -var List = require('list.js'); +var _ = require('lodash') + +var List = require('list.js') import { checkLoginStateChanged, setloginStateChangeEvent -} from './lib/common/login'; +} from './lib/common/login' import { debug, @@ -31,7 +35,7 @@ import { noteurl, urlpath, version -} from './lib/config'; +} from './lib/config' import { autoLinkify, @@ -53,14 +57,14 @@ import { updateLastChange, updateLastChangeUser, updateOwner -} from './extra'; +} from './extra' import { clearMap, setupSyncAreas, syncScrollToEdit, syncScrollToView -} from './syncscroll'; +} from './syncscroll' import { writeHistory, @@ -68,4007 +72,3856 @@ import { getHistory, saveHistory, removeHistory -} from './history'; +} from './history' -var renderer = require('./render'); -var preventXSS = renderer.preventXSS; +var renderer = require('./render') +var preventXSS = renderer.preventXSS -var defaultTextHeight = 20; -var viewportMargin = 20; -var mac = CodeMirror.keyMap["default"] == CodeMirror.keyMap.macDefault; -var defaultEditorMode = 'gfm'; +var defaultTextHeight = 20 +var viewportMargin = 20 +var mac = CodeMirror.keyMap['default'] === CodeMirror.keyMap.macDefault +var defaultEditorMode = 'gfm' var defaultExtraKeys = { - "F10": function (cm) { - cm.setOption("fullScreen", !cm.getOption("fullScreen")); - }, - "Esc": function (cm) { - if (cm.getOption('keyMap').substr(0, 3) === 'vim') return CodeMirror.Pass; - else if (cm.getOption("fullScreen")) cm.setOption("fullScreen", false); - }, - "Cmd-S": function () { - return false; - }, - "Ctrl-S": function () { - return false; - }, - "Enter": "newlineAndIndentContinueMarkdownList", - "Tab": function (cm) { - var tab = '\t'; - var spaces = Array(parseInt(cm.getOption("indentUnit")) + 1).join(" "); - //auto indent whole line when in list or blockquote - var cursor = cm.getCursor(); - var line = cm.getLine(cursor.line); - var regex = /^(\s*)(>[> ]*|[*+-]\s|(\d+)([.)]))/; - var match; - var multiple = cm.getSelection().split('\n').length > 1 || cm.getSelections().length > 1; - if (multiple) { - cm.execCommand('defaultTab'); - } else if ((match = regex.exec(line)) !== null) { - var ch = match[1].length; - var pos = { - line: cursor.line, - ch: ch - }; - if (cm.getOption('indentWithTabs')) - cm.replaceRange(tab, pos, pos, '+input'); - else - cm.replaceRange(spaces, pos, pos, '+input'); - } else { - if (cm.getOption('indentWithTabs')) - cm.execCommand('defaultTab'); - else { - cm.replaceSelection(spaces); - } - } - }, - "Cmd-Left": "goLineLeftSmart", - "Cmd-Right": "goLineRight", - "Ctrl-C": function (cm) { - if (!mac && cm.getOption('keyMap').substr(0, 3) === 'vim') document.execCommand("copy"); - else return CodeMirror.Pass; - }, - "Ctrl-*": function (cm) { - wrapTextWith(cm, '*'); - }, - "Shift-Ctrl-8": function (cm) { - wrapTextWith(cm, '*'); - }, - "Ctrl-_": function (cm) { - wrapTextWith(cm, '_'); - }, - "Shift-Ctrl--": function (cm) { - wrapTextWith(cm, '_'); - }, - "Ctrl-~": function (cm) { - wrapTextWith(cm, '~'); - }, - "Shift-Ctrl-`": function (cm) { - wrapTextWith(cm, '~'); - }, - "Ctrl-^": function (cm) { - wrapTextWith(cm, '^'); - }, - "Shift-Ctrl-6": function (cm) { - wrapTextWith(cm, '^'); - }, - "Ctrl-+": function (cm) { - wrapTextWith(cm, '+'); - }, - "Shift-Ctrl-=": function (cm) { - wrapTextWith(cm, '+'); - }, - "Ctrl-=": function (cm) { - wrapTextWith(cm, '='); - }, - "Shift-Ctrl-Backspace": function (cm) { - wrapTextWith(cm, 'Backspace'); - } -}; - -var wrapSymbols = ['*', '_', '~', '^', '+', '=']; - -function wrapTextWith(cm, symbol) { - if (!cm.getSelection()) { - return CodeMirror.Pass; + 'F10': function (cm) { + cm.setOption('fullScreen', !cm.getOption('fullScreen')) + }, + 'Esc': function (cm) { + if (cm.getOption('keyMap').substr(0, 3) === 'vim') return CodeMirror.Pass + else if (cm.getOption('fullScreen')) cm.setOption('fullScreen', false) + }, + 'Cmd-S': function () { + return false + }, + 'Ctrl-S': function () { + return false + }, + 'Enter': 'newlineAndIndentContinueMarkdownList', + 'Tab': function (cm) { + var tab = '\t' + var spaces = Array(parseInt(cm.getOption('indentUnit')) + 1).join(' ') + // auto indent whole line when in list or blockquote + var cursor = cm.getCursor() + var line = cm.getLine(cursor.line) + var regex = /^(\s*)(>[> ]*|[*+-]\s|(\d+)([.)]))/ + var match + var multiple = cm.getSelection().split('\n').length > 1 || cm.getSelections().length > 1 + if (multiple) { + cm.execCommand('defaultTab') + } else if ((match = regex.exec(line)) !== null) { + var ch = match[1].length + var pos = { + line: cursor.line, + ch: ch + } + if (cm.getOption('indentWithTabs')) { cm.replaceRange(tab, pos, pos, '+input') } else { cm.replaceRange(spaces, pos, pos, '+input') } } else { - var ranges = cm.listSelections(); - for (var i = 0; i < ranges.length; i++) { - var range = ranges[i]; - if (!range.empty()) { - var from = range.from(), to = range.to(); - if (symbol !== 'Backspace') { - cm.replaceRange(symbol, to, to, '+input'); - cm.replaceRange(symbol, from, from, '+input'); + if (cm.getOption('indentWithTabs')) { cm.execCommand('defaultTab') } else { + cm.replaceSelection(spaces) + } + } + }, + 'Cmd-Left': 'goLineLeftSmart', + 'Cmd-Right': 'goLineRight', + 'Ctrl-C': function (cm) { + if (!mac && cm.getOption('keyMap').substr(0, 3) === 'vim') document.execCommand('copy') + else return CodeMirror.Pass + }, + 'Ctrl-*': function (cm) { + wrapTextWith(cm, '*') + }, + 'Shift-Ctrl-8': function (cm) { + wrapTextWith(cm, '*') + }, + 'Ctrl-_': function (cm) { + wrapTextWith(cm, '_') + }, + 'Shift-Ctrl--': function (cm) { + wrapTextWith(cm, '_') + }, + 'Ctrl-~': function (cm) { + wrapTextWith(cm, '~') + }, + 'Shift-Ctrl-`': function (cm) { + wrapTextWith(cm, '~') + }, + 'Ctrl-^': function (cm) { + wrapTextWith(cm, '^') + }, + 'Shift-Ctrl-6': function (cm) { + wrapTextWith(cm, '^') + }, + 'Ctrl-+': function (cm) { + wrapTextWith(cm, '+') + }, + 'Shift-Ctrl-=': function (cm) { + wrapTextWith(cm, '+') + }, + 'Ctrl-=': function (cm) { + wrapTextWith(cm, '=') + }, + 'Shift-Ctrl-Backspace': function (cm) { + wrapTextWith(cm, 'Backspace') + } +} + +var wrapSymbols = ['*', '_', '~', '^', '+', '='] + +function wrapTextWith (cm, symbol) { + if (!cm.getSelection()) { + return CodeMirror.Pass + } else { + var ranges = cm.listSelections() + for (var i = 0; i < ranges.length; i++) { + var range = ranges[i] + if (!range.empty()) { + var from = range.from() + var to = range.to() + if (symbol !== 'Backspace') { + cm.replaceRange(symbol, to, to, '+input') + cm.replaceRange(symbol, from, from, '+input') // workaround selection range not correct after add symbol - var _ranges = cm.listSelections(); - var anchorIndex = editor.indexFromPos(_ranges[i].anchor); - var headIndex = editor.indexFromPos(_ranges[i].head); - if (anchorIndex > headIndex) { - _ranges[i].anchor.ch--; - } else { - _ranges[i].head.ch--; - } - cm.setSelections(_ranges); - } else { - var preEndPos = { - line: to.line, - ch: to.ch + 1 - }; - var preText = cm.getRange(to, preEndPos); - var preIndex = wrapSymbols.indexOf(preText); - var postEndPos = { - line: from.line, - ch: from.ch - 1 - }; - var postText = cm.getRange(postEndPos, from); - var postIndex = wrapSymbols.indexOf(postText); + var _ranges = cm.listSelections() + var anchorIndex = window.editor.indexFromPos(_ranges[i].anchor) + var headIndex = window.editor.indexFromPos(_ranges[i].head) + if (anchorIndex > headIndex) { + _ranges[i].anchor.ch-- + } else { + _ranges[i].head.ch-- + } + cm.setSelections(_ranges) + } else { + var preEndPos = { + line: to.line, + ch: to.ch + 1 + } + var preText = cm.getRange(to, preEndPos) + var preIndex = wrapSymbols.indexOf(preText) + var postEndPos = { + line: from.line, + ch: from.ch - 1 + } + var postText = cm.getRange(postEndPos, from) + var postIndex = wrapSymbols.indexOf(postText) // check if surround symbol are list in array and matched - if (preIndex > -1 && postIndex > -1 && preIndex === postIndex) { - cm.replaceRange("", to, preEndPos, '+delete'); - cm.replaceRange("", postEndPos, from, '+delete'); - } - } - } + if (preIndex > -1 && postIndex > -1 && preIndex === postIndex) { + cm.replaceRange('', to, preEndPos, '+delete') + cm.replaceRange('', postEndPos, from, '+delete') + } } + } } + } } -var idleTime = 300000; //5 mins -var updateViewDebounce = 100; -var cursorMenuThrottle = 50; -var cursorActivityDebounce = 50; -var cursorAnimatePeriod = 100; -var supportContainers = ['success', 'info', 'warning', 'danger']; -var supportCodeModes = ['javascript', 'typescript', 'jsx', 'htmlmixed', 'htmlembedded', 'css', 'xml', 'clike', 'clojure', 'ruby', 'python', 'shell', 'php', 'sql', 'haskell', 'coffeescript', 'yaml', 'pug', 'lua', 'cmake', 'nginx', 'perl', 'sass', 'r', 'dockerfile', 'tiddlywiki', 'mediawiki', 'go']; -var supportCharts = ['sequence', 'flow', 'graphviz', 'mermaid']; +var idleTime = 300000 // 5 mins +var updateViewDebounce = 100 +var cursorMenuThrottle = 50 +var cursorActivityDebounce = 50 +var cursorAnimatePeriod = 100 +var supportContainers = ['success', 'info', 'warning', 'danger'] +var supportCodeModes = ['javascript', 'typescript', 'jsx', 'htmlmixed', 'htmlembedded', 'css', 'xml', 'clike', 'clojure', 'ruby', 'python', 'shell', 'php', 'sql', 'haskell', 'coffeescript', 'yaml', 'pug', 'lua', 'cmake', 'nginx', 'perl', 'sass', 'r', 'dockerfile', 'tiddlywiki', 'mediawiki', 'go'] +var supportCharts = ['sequence', 'flow', 'graphviz', 'mermaid'] var supportHeaders = [ - { - text: '# h1', - search: '#' - }, - { - text: '## h2', - search: '##' - }, - { - text: '### h3', - search: '###' - }, - { - text: '#### h4', - search: '####' - }, - { - text: '##### h5', - search: '#####' - }, - { - text: '###### h6', - search: '######' - }, - { - text: '###### tags: `example`', - search: '###### tags:' - } -]; + { + text: '# h1', + search: '#' + }, + { + text: '## h2', + search: '##' + }, + { + text: '### h3', + search: '###' + }, + { + text: '#### h4', + search: '####' + }, + { + text: '##### h5', + search: '#####' + }, + { + text: '###### h6', + search: '######' + }, + { + text: '###### tags: `example`', + search: '###### tags:' + } +] var supportReferrals = [ - { - text: '[reference link]', - search: '[]' - }, - { - text: '[reference]: https:// "title"', - search: '[]:' - }, - { - text: '[^footnote link]', - search: '[^]' - }, - { - text: '[^footnote reference]: https:// "title"', - search: '[^]:' - }, - { - text: '^[inline footnote]', - search: '^[]' - }, - { - text: '[link text][reference]', - search: '[][]' - }, - { - text: '[link text](https:// "title")', - search: '[]()' - }, - { - text: '![image alt][reference]', - search: '![][]' - }, - { - text: '![image alt](https:// "title")', - search: '![]()' - }, - { - text: '![image alt](https:// "title" =WidthxHeight)', - search: '![]()' - }, - { - text: '[TOC]', - search: '[]' - } -]; + { + text: '[reference link]', + search: '[]' + }, + { + text: '[reference]: https:// "title"', + search: '[]:' + }, + { + text: '[^footnote link]', + search: '[^]' + }, + { + text: '[^footnote reference]: https:// "title"', + search: '[^]:' + }, + { + text: '^[inline footnote]', + search: '^[]' + }, + { + text: '[link text][reference]', + search: '[][]' + }, + { + text: '[link text](https:// "title")', + search: '[]()' + }, + { + text: '![image alt][reference]', + search: '![][]' + }, + { + text: '![image alt](https:// "title")', + search: '![]()' + }, + { + text: '![image alt](https:// "title" =WidthxHeight)', + search: '![]()' + }, + { + text: '[TOC]', + search: '[]' + } +] var supportExternals = [ - { - text: '{%youtube youtubeid %}', - search: 'youtube' - }, - { - text: '{%vimeo vimeoid %}', - search: 'vimeo' - }, - { - text: '{%gist gistid %}', - search: 'gist' - }, - { - text: '{%slideshare slideshareid %}', - search: 'slideshare' - }, - { - text: '{%speakerdeck speakerdeckid %}', - search: 'speakerdeck' - }, - { - text: '{%pdf pdfurl %}', - search: 'pdf' - } -]; + { + text: '{%youtube youtubeid %}', + search: 'youtube' + }, + { + text: '{%vimeo vimeoid %}', + search: 'vimeo' + }, + { + text: '{%gist gistid %}', + search: 'gist' + }, + { + text: '{%slideshare slideshareid %}', + search: 'slideshare' + }, + { + text: '{%speakerdeck speakerdeckid %}', + search: 'speakerdeck' + }, + { + text: '{%pdf pdfurl %}', + search: 'pdf' + } +] var supportExtraTags = [ - { - text: '[name tag]', - search: '[]', - command: function () { - return '[name=' + personalInfo.name + ']'; - }, - }, - { - text: '[time tag]', - search: '[]', - command: function () { - return '[time=' + moment().format('llll') + ']'; - }, - }, - { - text: '[my color tag]', - search: '[]', - command: function () { - return '[color=' + personalInfo.color + ']'; - } - }, - { - text: '[random color tag]', - search: '[]', - command: function () { - var color = randomColor(); - return '[color=' + color + ']'; - } - } -]; + { + text: '[name tag]', + search: '[]', + command: function () { + return '[name=' + window.personalInfo.name + ']' + } + }, + { + text: '[time tag]', + search: '[]', + command: function () { + return '[time=' + moment().format('llll') + ']' + } + }, + { + text: '[my color tag]', + search: '[]', + command: function () { + return '[color=' + window.personalInfo.color + ']' + } + }, + { + text: '[random color tag]', + search: '[]', + command: function () { + var color = randomColor() + return '[color=' + color + ']' + } + } +] window.modeType = { - edit: { - name: "edit" - }, - view: { - name: "view" - }, - both: { - name: "both" - } -}; + edit: { + name: 'edit' + }, + view: { + name: 'view' + }, + both: { + name: 'both' + } +} var statusType = { - connected: { - msg: "CONNECTED", - label: "label-warning", - fa: "fa-wifi" - }, - online: { - msg: "ONLINE", - label: "label-primary", - fa: "fa-users" - }, - offline: { - msg: "OFFLINE", - label: "label-danger", - fa: "fa-plug" - } -}; -var defaultMode = modeType.view; - -//global vars -window.loaded = false; -window.needRefresh = false; -window.isDirty = false; -window.editShown = false; -window.visibleXS = false; -window.visibleSM = false; -window.visibleMD = false; -window.visibleLG = false; -window.isTouchDevice = 'ontouchstart' in document.documentElement; -window.currentMode = defaultMode; -window.currentStatus = statusType.offline; + connected: { + msg: 'CONNECTED', + label: 'label-warning', + fa: 'fa-wifi' + }, + online: { + msg: 'ONLINE', + label: 'label-primary', + fa: 'fa-users' + }, + offline: { + msg: 'OFFLINE', + label: 'label-danger', + fa: 'fa-plug' + } +} +var defaultMode = modeType.view + +// global vars +window.loaded = false +window.needRefresh = false +window.isDirty = false +window.editShown = false +window.visibleXS = false +window.visibleSM = false +window.visibleMD = false +window.visibleLG = false +window.isTouchDevice = 'ontouchstart' in document.documentElement +window.currentMode = defaultMode +window.currentStatus = statusType.offline window.lastInfo = { - needRestore: false, - cursor: null, - scroll: null, - edit: { - scroll: { - left: null, - top: null - }, - cursor: { - line: null, - ch: null - }, - selections: null + needRestore: false, + cursor: null, + scroll: null, + edit: { + scroll: { + left: null, + top: null }, - view: { - scroll: { - left: null, - top: null - } + cursor: { + line: null, + ch: null }, - history: null -}; -window.personalInfo = {}; -window.onlineUsers = []; + selections: null + }, + view: { + scroll: { + left: null, + top: null + } + }, + history: null +} +window.personalInfo = {} +window.onlineUsers = [] window.fileTypes = { - "pl": "perl", - "cgi": "perl", - "js": "javascript", - "php": "php", - "sh": "bash", - "rb": "ruby", - "html": "html", - "py": "python" -}; - -//editor settings -var textit = document.getElementById("textit"); -if (!textit) throw new Error("There was no textit area!"); + 'pl': 'perl', + 'cgi': 'perl', + 'js': 'javascript', + 'php': 'php', + 'sh': 'bash', + 'rb': 'ruby', + 'html': 'html', + 'py': 'python' +} + +// editor settings +var textit = document.getElementById('textit') +if (!textit) throw new Error('There was no textit area!') window.editor = CodeMirror.fromTextArea(textit, { - mode: defaultEditorMode, - backdrop: defaultEditorMode, - keyMap: "sublime", - viewportMargin: viewportMargin, - styleActiveLine: true, - lineNumbers: true, - lineWrapping: true, - showCursorWhenSelecting: true, - highlightSelectionMatches: true, - indentUnit: 4, - continueComments: "Enter", - theme: "one-dark", - inputStyle: "textarea", - matchBrackets: true, - autoCloseBrackets: true, - matchTags: { - bothTags: true - }, - autoCloseTags: true, - foldGutter: true, - gutters: ["CodeMirror-linenumbers", "authorship-gutters", "CodeMirror-foldgutter"], - extraKeys: defaultExtraKeys, - flattenSpans: true, - addModeClass: true, - readOnly: true, - autoRefresh: true, - otherCursors: true, - placeholder: "← Start by entering a title here\n===\nVisit /features if you don't know what to do.\nHappy hacking :)" -}); -var inlineAttach = inlineAttachment.editors.codemirror4.attach(editor); -defaultTextHeight = parseInt($(".CodeMirror").css('line-height')); - -var statusBarTemplate = null; -var statusBar = null; -var statusPanel = null; -var statusCursor = null; -var statusFile = null; -var statusIndicators = null; -var statusLength = null; -var statusKeymap = null; -var statusIndent = null; -var statusTheme = null; -var statusSpellcheck = null; -var statusPreferences = null; - -function getStatusBarTemplate(callback) { - $.get(serverurl + '/views/statusbar.html', function (template) { - statusBarTemplate = template; - if (callback) callback(); - }); -} -getStatusBarTemplate(); - -function addStatusBar() { - if (!statusBarTemplate) { - getStatusBarTemplate(addStatusBar); - return; - } - statusBar = $(statusBarTemplate); - statusCursor = statusBar.find('.status-cursor'); - statusFile = statusBar.find('.status-file'); - statusIndicators = statusBar.find('.status-indicators'); - statusIndent = statusBar.find('.status-indent'); - statusKeymap = statusBar.find('.status-keymap'); - statusLength = statusBar.find('.status-length'); - statusTheme = statusBar.find('.status-theme'); - statusSpellcheck = statusBar.find('.status-spellcheck'); - statusPreferences = statusBar.find('.status-preferences'); - statusPanel = editor.addPanel(statusBar[0], { - position: "bottom" - }); - - setIndent(); - setKeymap(); - setTheme(); - setSpellcheck(); - setPreferences(); -} - -function setIndent() { - var cookieIndentType = Cookies.get('indent_type'); - var cookieTabSize = parseInt(Cookies.get('tab_size')); - var cookieSpaceUnits = parseInt(Cookies.get('space_units')); - if (cookieIndentType) { - if (cookieIndentType == 'tab') { - editor.setOption('indentWithTabs', true); - if (cookieTabSize) - editor.setOption('indentUnit', cookieTabSize); - } else if (cookieIndentType == 'space') { - editor.setOption('indentWithTabs', false); - if (cookieSpaceUnits) - editor.setOption('indentUnit', cookieSpaceUnits); - } - } - if (cookieTabSize) - editor.setOption('tabSize', cookieTabSize); - - var type = statusIndicators.find('.indent-type'); - var widthLabel = statusIndicators.find('.indent-width-label'); - var widthInput = statusIndicators.find('.indent-width-input'); - - function setType() { - if (editor.getOption('indentWithTabs')) { - Cookies.set('indent_type', 'tab', { - expires: 365 - }); - type.text('Tab Size:'); - } else { - Cookies.set('indent_type', 'space', { - expires: 365 - }); - type.text('Spaces:'); - } - } - setType(); - - function setUnit() { - var unit = editor.getOption('indentUnit'); - if (editor.getOption('indentWithTabs')) { - Cookies.set('tab_size', unit, { - expires: 365 - }); - } else { - Cookies.set('space_units', unit, { - expires: 365 - }); - } - widthLabel.text(unit); + mode: defaultEditorMode, + backdrop: defaultEditorMode, + keyMap: 'sublime', + viewportMargin: viewportMargin, + styleActiveLine: true, + lineNumbers: true, + lineWrapping: true, + showCursorWhenSelecting: true, + highlightSelectionMatches: true, + indentUnit: 4, + continueComments: 'Enter', + theme: 'one-dark', + inputStyle: 'textarea', + matchBrackets: true, + autoCloseBrackets: true, + matchTags: { + bothTags: true + }, + autoCloseTags: true, + foldGutter: true, + gutters: ['CodeMirror-linenumbers', 'authorship-gutters', 'CodeMirror-foldgutter'], + extraKeys: defaultExtraKeys, + flattenSpans: true, + addModeClass: true, + readOnly: true, + autoRefresh: true, + otherCursors: true, + placeholder: "← Start by entering a title here\n===\nVisit /features if you don't know what to do.\nHappy hacking :)" +}) +var inlineAttach = window.inlineAttachment.editors.codemirror4.attach(editor) +defaultTextHeight = parseInt($('.CodeMirror').css('line-height')) + +var statusBarTemplate = null +var statusBar = null +var statusCursor = null +var statusFile = null +var statusIndicators = null +var statusLength = null +var statusTheme = null +var statusSpellcheck = null + +function getStatusBarTemplate (callback) { + $.get(serverurl + '/views/statusbar.html', function (template) { + statusBarTemplate = template + if (callback) callback() + }) +} +getStatusBarTemplate() + +function addStatusBar () { + if (!statusBarTemplate) { + getStatusBarTemplate(addStatusBar) + return + } + statusBar = $(statusBarTemplate) + statusCursor = statusBar.find('.status-cursor') + statusFile = statusBar.find('.status-file') + statusIndicators = statusBar.find('.status-indicators') + statusBar.find('.status-indent') + statusBar.find('.status-keymap') + statusLength = statusBar.find('.status-length') + statusTheme = statusBar.find('.status-theme') + statusSpellcheck = statusBar.find('.status-spellcheck') + statusBar.find('.status-preferences') + editor.addPanel(statusBar[0], { + position: 'bottom' + }) + + setIndent() + setKeymap() + setTheme() + setSpellcheck() + setPreferences() +} + +function setIndent () { + var cookieIndentType = Cookies.get('indent_type') + var cookieTabSize = parseInt(Cookies.get('tab_size')) + var cookieSpaceUnits = parseInt(Cookies.get('space_units')) + if (cookieIndentType) { + if (cookieIndentType === 'tab') { + editor.setOption('indentWithTabs', true) + if (cookieTabSize) { editor.setOption('indentUnit', cookieTabSize) } + } else if (cookieIndentType === 'space') { + editor.setOption('indentWithTabs', false) + if (cookieSpaceUnits) { editor.setOption('indentUnit', cookieSpaceUnits) } + } + } + if (cookieTabSize) { editor.setOption('tabSize', cookieTabSize) } + + var type = statusIndicators.find('.indent-type') + var widthLabel = statusIndicators.find('.indent-width-label') + var widthInput = statusIndicators.find('.indent-width-input') + + function setType () { + if (editor.getOption('indentWithTabs')) { + Cookies.set('indent_type', 'tab', { + expires: 365 + }) + type.text('Tab Size:') + } else { + Cookies.set('indent_type', 'space', { + expires: 365 + }) + type.text('Spaces:') + } + } + setType() + + function setUnit () { + var unit = editor.getOption('indentUnit') + if (editor.getOption('indentWithTabs')) { + Cookies.set('tab_size', unit, { + expires: 365 + }) + } else { + Cookies.set('space_units', unit, { + expires: 365 + }) + } + widthLabel.text(unit) + } + setUnit() + + type.click(function () { + if (editor.getOption('indentWithTabs')) { + editor.setOption('indentWithTabs', false) + cookieSpaceUnits = parseInt(Cookies.get('space_units')) + if (cookieSpaceUnits) { editor.setOption('indentUnit', cookieSpaceUnits) } + } else { + editor.setOption('indentWithTabs', true) + cookieTabSize = parseInt(Cookies.get('tab_size')) + if (cookieTabSize) { + editor.setOption('indentUnit', cookieTabSize) + editor.setOption('tabSize', cookieTabSize) + } + } + setType() + setUnit() + }) + widthLabel.click(function () { + if (widthLabel.is(':visible')) { + widthLabel.addClass('hidden') + widthInput.removeClass('hidden') + widthInput.val(editor.getOption('indentUnit')) + widthInput.select() + } else { + widthLabel.removeClass('hidden') + widthInput.addClass('hidden') + } + }) + widthInput.on('change', function () { + var val = parseInt(widthInput.val()) + if (!val) val = editor.getOption('indentUnit') + if (val < 1) val = 1 + else if (val > 10) val = 10 + + if (editor.getOption('indentWithTabs')) { + editor.setOption('tabSize', val) + } + editor.setOption('indentUnit', val) + setUnit() + }) + widthInput.on('blur', function () { + widthLabel.removeClass('hidden') + widthInput.addClass('hidden') + }) +} + +function setKeymap () { + var cookieKeymap = Cookies.get('keymap') + if (cookieKeymap) { editor.setOption('keyMap', cookieKeymap) } + + var label = statusIndicators.find('.ui-keymap-label') + var sublime = statusIndicators.find('.ui-keymap-sublime') + var emacs = statusIndicators.find('.ui-keymap-emacs') + var vim = statusIndicators.find('.ui-keymap-vim') + + function setKeymapLabel () { + var keymap = editor.getOption('keyMap') + Cookies.set('keymap', keymap, { + expires: 365 + }) + label.text(keymap) + restoreOverrideEditorKeymap() + setOverrideBrowserKeymap() + } + setKeymapLabel() + + sublime.click(function () { + editor.setOption('keyMap', 'sublime') + setKeymapLabel() + }) + emacs.click(function () { + editor.setOption('keyMap', 'emacs') + setKeymapLabel() + }) + vim.click(function () { + editor.setOption('keyMap', 'vim') + setKeymapLabel() + }) +} + +function setTheme () { + var cookieTheme = Cookies.get('theme') + if (cookieTheme) { + editor.setOption('theme', cookieTheme) + } + + var themeToggle = statusTheme.find('.ui-theme-toggle') + themeToggle.click(function () { + var theme = editor.getOption('theme') + if (theme === 'one-dark') { + theme = 'default' + } else { + theme = 'one-dark' } - setUnit(); - - type.click(function () { - if (editor.getOption('indentWithTabs')) { - editor.setOption('indentWithTabs', false); - cookieSpaceUnits = parseInt(Cookies.get('space_units')); - if (cookieSpaceUnits) - editor.setOption('indentUnit', cookieSpaceUnits) - } else { - editor.setOption('indentWithTabs', true); - cookieTabSize = parseInt(Cookies.get('tab_size')); - if (cookieTabSize) { - editor.setOption('indentUnit', cookieTabSize); - editor.setOption('tabSize', cookieTabSize); - } - } - setType(); - setUnit(); - }); - widthLabel.click(function () { - if (widthLabel.is(':visible')) { - widthLabel.addClass('hidden'); - widthInput.removeClass('hidden'); - widthInput.val(editor.getOption('indentUnit')); - widthInput.select(); - } else { - widthLabel.removeClass('hidden'); - widthInput.addClass('hidden'); - } - }); - widthInput.on('change', function () { - var val = parseInt(widthInput.val()); - if (!val) val = editor.getOption('indentUnit'); - if (val < 1) val = 1; - else if (val > 10) val = 10; - - if (editor.getOption('indentWithTabs')) { - editor.setOption('tabSize', val); - } - editor.setOption('indentUnit', val); - setUnit(); - }); - widthInput.on('blur', function () { - widthLabel.removeClass('hidden'); - widthInput.addClass('hidden'); - }); -} - -function setKeymap() { - var cookieKeymap = Cookies.get('keymap'); - if (cookieKeymap) - editor.setOption('keyMap', cookieKeymap); - - var label = statusIndicators.find('.ui-keymap-label'); - var sublime = statusIndicators.find('.ui-keymap-sublime'); - var emacs = statusIndicators.find('.ui-keymap-emacs'); - var vim = statusIndicators.find('.ui-keymap-vim'); - - function setKeymapLabel() { - var keymap = editor.getOption('keyMap'); - Cookies.set('keymap', keymap, { - expires: 365 - }); - label.text(keymap); - restoreOverrideEditorKeymap(); - setOverrideBrowserKeymap(); - } - setKeymapLabel(); - - sublime.click(function () { - editor.setOption('keyMap', 'sublime'); - setKeymapLabel(); - }); - emacs.click(function () { - editor.setOption('keyMap', 'emacs'); - setKeymapLabel(); - }); - vim.click(function () { - editor.setOption('keyMap', 'vim'); - setKeymapLabel(); - }); -} - -function setTheme() { - var cookieTheme = Cookies.get('theme'); - if (cookieTheme) { - editor.setOption('theme', cookieTheme); - } - - var themeToggle = statusTheme.find('.ui-theme-toggle'); - themeToggle.click(function () { - var theme = editor.getOption('theme'); - if (theme == "one-dark") { - theme = "default"; - } else { - theme = "one-dark"; - } - editor.setOption('theme', theme); - Cookies.set('theme', theme, { - expires: 365 - }); - checkTheme(); - }); - function checkTheme() { - var theme = editor.getOption('theme'); - if (theme == "one-dark") { - themeToggle.removeClass('active'); - } else { - themeToggle.addClass('active'); - } + editor.setOption('theme', theme) + Cookies.set('theme', theme, { + expires: 365 + }) + checkTheme() + }) + function checkTheme () { + var theme = editor.getOption('theme') + if (theme === 'one-dark') { + themeToggle.removeClass('active') + } else { + themeToggle.addClass('active') } - checkTheme(); + } + checkTheme() } -function setSpellcheck() { - var cookieSpellcheck = Cookies.get('spellcheck'); - if (cookieSpellcheck) { - var mode = null; - if (cookieSpellcheck === 'true' || cookieSpellcheck === true) { - mode = 'spell-checker'; - } else { - mode = defaultEditorMode; - } - if (mode && mode !== editor.getOption('mode')) { - editor.setOption('mode', mode); - } - } - - var spellcheckToggle = statusSpellcheck.find('.ui-spellcheck-toggle'); - spellcheckToggle.click(function () { - var mode = editor.getOption('mode'); - if (mode == defaultEditorMode) { - mode = "spell-checker"; - } else { - mode = defaultEditorMode; - } - if (mode && mode !== editor.getOption('mode')) { - editor.setOption('mode', mode); - } - Cookies.set('spellcheck', (mode == "spell-checker"), { - expires: 365 - }); - checkSpellcheck(); - }); - function checkSpellcheck() { - var mode = editor.getOption('mode'); - if (mode == defaultEditorMode) { - spellcheckToggle.removeClass('active'); - } else { - spellcheckToggle.addClass('active'); - } +function setSpellcheck () { + var cookieSpellcheck = Cookies.get('spellcheck') + if (cookieSpellcheck) { + var mode = null + if (cookieSpellcheck === 'true' || cookieSpellcheck === true) { + mode = 'spell-checker' + } else { + mode = defaultEditorMode } - checkSpellcheck(); - - //workaround spellcheck might not activate beacuse the ajax loading - if (num_loaded < 2) { - var spellcheckTimer = setInterval(function () { - if (num_loaded >= 2) { - if (editor.getOption('mode') == "spell-checker") - editor.setOption('mode', "spell-checker"); - clearInterval(spellcheckTimer); - } - }, 100); + if (mode && mode !== editor.getOption('mode')) { + editor.setOption('mode', mode) } -} + } -var jumpToAddressBarKeymapName = mac ? "Cmd-L" : "Ctrl-L"; -var jumpToAddressBarKeymapValue = null; -function resetEditorKeymapToBrowserKeymap() { - var keymap = editor.getOption('keyMap'); - if (!jumpToAddressBarKeymapValue) { - jumpToAddressBarKeymapValue = CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName]; - delete CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName]; - } -} -function restoreOverrideEditorKeymap() { - var keymap = editor.getOption('keyMap'); - if (jumpToAddressBarKeymapValue) { - CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName] = jumpToAddressBarKeymapValue; - jumpToAddressBarKeymapValue = null; - } -} -function setOverrideBrowserKeymap() { - var overrideBrowserKeymap = $('.ui-preferences-override-browser-keymap label > input[type="checkbox"]'); - if(overrideBrowserKeymap.is(":checked")) { - Cookies.set('preferences-override-browser-keymap', true, { - expires: 365 - }); - restoreOverrideEditorKeymap(); + var spellcheckToggle = statusSpellcheck.find('.ui-spellcheck-toggle') + spellcheckToggle.click(function () { + var mode = editor.getOption('mode') + if (mode === defaultEditorMode) { + mode = 'spell-checker' } else { - Cookies.remove('preferences-override-browser-keymap'); - resetEditorKeymapToBrowserKeymap(); + mode = defaultEditorMode } -} - -function setPreferences() { - var overrideBrowserKeymap = $('.ui-preferences-override-browser-keymap label > input[type="checkbox"]'); - var cookieOverrideBrowserKeymap = Cookies.get('preferences-override-browser-keymap'); - if (cookieOverrideBrowserKeymap && cookieOverrideBrowserKeymap === "true") { - overrideBrowserKeymap.prop('checked', true); - } else { - overrideBrowserKeymap.prop('checked', false); + if (mode && mode !== editor.getOption('mode')) { + editor.setOption('mode', mode) } - setOverrideBrowserKeymap(); - - overrideBrowserKeymap.change(function() { - setOverrideBrowserKeymap(); - }); -} - -var selection = null; - -function updateStatusBar() { - if (!statusBar) return; - var cursor = editor.getCursor(); - var cursorText = 'Line ' + (cursor.line + 1) + ', Columns ' + (cursor.ch + 1); - if (selection) { - var anchor = selection.anchor; - var head = selection.head; - var start = head.line <= anchor.line ? head : anchor; - var end = head.line >= anchor.line ? head : anchor; - var selectionText = ' — Selected '; - var selectionCharCount = Math.abs(head.ch - anchor.ch); - // borrow from brackets EditorStatusBar.js - if (start.line !== end.line) { - var lines = end.line - start.line + 1; - if (end.ch === 0) { - lines--; - } - selectionText += lines + ' lines'; - } else if (selectionCharCount > 0) - selectionText += selectionCharCount + ' columns'; - if (start.line !== end.line || selectionCharCount > 0) - cursorText += selectionText; - } - statusCursor.text(cursorText); - var fileText = ' — ' + editor.lineCount() + ' Lines'; - statusFile.text(fileText); - var docLength = editor.getValue().length; - statusLength.text('Length ' + docLength); - if (docLength > (docmaxlength * 0.95)) { - statusLength.css('color', 'red'); - statusLength.attr('title', 'Your almost reach note max length limit.'); - } else if (docLength > (docmaxlength * 0.8)) { - statusLength.css('color', 'orange'); - statusLength.attr('title', 'You nearly fill the note, consider to make more pieces.'); + Cookies.set('spellcheck', (mode === 'spell-checker'), { + expires: 365 + }) + checkSpellcheck() + }) + function checkSpellcheck () { + var mode = editor.getOption('mode') + if (mode === defaultEditorMode) { + spellcheckToggle.removeClass('active') } else { - statusLength.css('color', 'white'); - statusLength.attr('title', 'You could write up to ' + docmaxlength + ' characters in this note.'); - } -} - -//ui vars + spellcheckToggle.addClass('active') + } + } + checkSpellcheck() + + // workaround spellcheck might not activate beacuse the ajax loading + /* eslint-disable camelcase */ + if (num_loaded < 2) { + var spellcheckTimer = setInterval(function () { + if (num_loaded >= 2) { + if (editor.getOption('mode') === 'spell-checker') { editor.setOption('mode', 'spell-checker') } + clearInterval(spellcheckTimer) + } + }, 100) + } + /* eslint-endable camelcase */ +} + +var jumpToAddressBarKeymapName = mac ? 'Cmd-L' : 'Ctrl-L' +var jumpToAddressBarKeymapValue = null +function resetEditorKeymapToBrowserKeymap () { + var keymap = editor.getOption('keyMap') + if (!jumpToAddressBarKeymapValue) { + jumpToAddressBarKeymapValue = CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName] + delete CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName] + } +} +function restoreOverrideEditorKeymap () { + var keymap = editor.getOption('keyMap') + if (jumpToAddressBarKeymapValue) { + CodeMirror.keyMap[keymap][jumpToAddressBarKeymapName] = jumpToAddressBarKeymapValue + jumpToAddressBarKeymapValue = null + } +} +function setOverrideBrowserKeymap () { + var overrideBrowserKeymap = $('.ui-preferences-override-browser-keymap label > input[type="checkbox"]') + if (overrideBrowserKeymap.is(':checked')) { + Cookies.set('preferences-override-browser-keymap', true, { + expires: 365 + }) + restoreOverrideEditorKeymap() + } else { + Cookies.remove('preferences-override-browser-keymap') + resetEditorKeymapToBrowserKeymap() + } +} + +function setPreferences () { + var overrideBrowserKeymap = $('.ui-preferences-override-browser-keymap label > input[type="checkbox"]') + var cookieOverrideBrowserKeymap = Cookies.get('preferences-override-browser-keymap') + if (cookieOverrideBrowserKeymap && cookieOverrideBrowserKeymap === 'true') { + overrideBrowserKeymap.prop('checked', true) + } else { + overrideBrowserKeymap.prop('checked', false) + } + setOverrideBrowserKeymap() + + overrideBrowserKeymap.change(function () { + setOverrideBrowserKeymap() + }) +} + +var selection = null + +function updateStatusBar () { + if (!statusBar) return + var cursor = editor.getCursor() + var cursorText = 'Line ' + (cursor.line + 1) + ', Columns ' + (cursor.ch + 1) + if (selection) { + var anchor = selection.anchor + var head = selection.head + var start = head.line <= anchor.line ? head : anchor + var end = head.line >= anchor.line ? head : anchor + var selectionText = ' — Selected ' + var selectionCharCount = Math.abs(head.ch - anchor.ch) + // borrow from brackets EditorStatusBar.js + if (start.line !== end.line) { + var lines = end.line - start.line + 1 + if (end.ch === 0) { + lines-- + } + selectionText += lines + ' lines' + } else if (selectionCharCount > 0) { selectionText += selectionCharCount + ' columns' } + if (start.line !== end.line || selectionCharCount > 0) { cursorText += selectionText } + } + statusCursor.text(cursorText) + var fileText = ' — ' + editor.lineCount() + ' Lines' + statusFile.text(fileText) + var docLength = editor.getValue().length + statusLength.text('Length ' + docLength) + if (docLength > (docmaxlength * 0.95)) { + statusLength.css('color', 'red') + statusLength.attr('title', 'Your almost reach note max length limit.') + } else if (docLength > (docmaxlength * 0.8)) { + statusLength.css('color', 'orange') + statusLength.attr('title', 'You nearly fill the note, consider to make more pieces.') + } else { + statusLength.css('color', 'white') + statusLength.attr('title', 'You could write up to ' + docmaxlength + ' characters in this note.') + } +} + +// ui vars window.ui = { - spinner: $(".ui-spinner"), - content: $(".ui-content"), - toolbar: { - shortStatus: $(".ui-short-status"), - status: $(".ui-status"), - new: $(".ui-new"), - publish: $(".ui-publish"), - extra: { - revision: $(".ui-extra-revision"), - slide: $(".ui-extra-slide") - }, - download: { - markdown: $(".ui-download-markdown"), - html: $(".ui-download-html"), - rawhtml: $(".ui-download-raw-html"), - pdf: $(".ui-download-pdf-beta"), - }, - export: { - dropbox: $(".ui-save-dropbox"), - googleDrive: $(".ui-save-google-drive"), - gist: $(".ui-save-gist"), - snippet: $(".ui-save-snippet") - }, - import: { - dropbox: $(".ui-import-dropbox"), - googleDrive: $(".ui-import-google-drive"), - gist: $(".ui-import-gist"), - snippet: $(".ui-import-snippet"), - clipboard: $(".ui-import-clipboard") - }, - mode: $(".ui-mode"), - edit: $(".ui-edit"), - view: $(".ui-view"), - both: $(".ui-both"), - uploadImage: $(".ui-upload-image") + spinner: $('.ui-spinner'), + content: $('.ui-content'), + toolbar: { + shortStatus: $('.ui-short-status'), + status: $('.ui-status'), + new: $('.ui-new'), + publish: $('.ui-publish'), + extra: { + revision: $('.ui-extra-revision'), + slide: $('.ui-extra-slide') }, - infobar: { - lastchange: $(".ui-lastchange"), - lastchangeuser: $(".ui-lastchangeuser"), - nolastchangeuser: $(".ui-no-lastchangeuser"), - permission: { - permission: $(".ui-permission"), - label: $(".ui-permission-label"), - freely: $(".ui-permission-freely"), - editable: $(".ui-permission-editable"), - locked: $(".ui-permission-locked"), - private: $(".ui-permission-private"), - limited: $(".ui-permission-limited"), - protected: $(".ui-permission-protected") - }, - delete: $(".ui-delete-note") + download: { + markdown: $('.ui-download-markdown'), + html: $('.ui-download-html'), + rawhtml: $('.ui-download-raw-html'), + pdf: $('.ui-download-pdf-beta') }, - toc: { - toc: $('.ui-toc'), - affix: $('.ui-affix-toc'), - label: $('.ui-toc-label'), - dropdown: $('.ui-toc-dropdown') + export: { + dropbox: $('.ui-save-dropbox'), + googleDrive: $('.ui-save-google-drive'), + gist: $('.ui-save-gist'), + snippet: $('.ui-save-snippet') }, - area: { - edit: $(".ui-edit-area"), - view: $(".ui-view-area"), - codemirror: $(".ui-edit-area .CodeMirror"), - codemirrorScroll: $(".ui-edit-area .CodeMirror .CodeMirror-scroll"), - codemirrorSizer: $(".ui-edit-area .CodeMirror .CodeMirror-sizer"), - codemirrorSizerInner: $(".ui-edit-area .CodeMirror .CodeMirror-sizer > div"), - markdown: $(".ui-view-area .markdown-body"), - resize: { - handle: $('.ui-resizable-handle'), - syncToggle: $('.ui-sync-toggle') - } + import: { + dropbox: $('.ui-import-dropbox'), + googleDrive: $('.ui-import-google-drive'), + gist: $('.ui-import-gist'), + snippet: $('.ui-import-snippet'), + clipboard: $('.ui-import-clipboard') }, - modal: { - snippetImportProjects: $("#snippetImportModalProjects"), - snippetImportSnippets: $("#snippetImportModalSnippets"), - revision: $("#revisionModal") - } -}; - -//page actions + mode: $('.ui-mode'), + edit: $('.ui-edit'), + view: $('.ui-view'), + both: $('.ui-both'), + uploadImage: $('.ui-upload-image') + }, + infobar: { + lastchange: $('.ui-lastchange'), + lastchangeuser: $('.ui-lastchangeuser'), + nolastchangeuser: $('.ui-no-lastchangeuser'), + permission: { + permission: $('.ui-permission'), + label: $('.ui-permission-label'), + freely: $('.ui-permission-freely'), + editable: $('.ui-permission-editable'), + locked: $('.ui-permission-locked'), + private: $('.ui-permission-private'), + limited: $('.ui-permission-limited'), + protected: $('.ui-permission-protected') + }, + delete: $('.ui-delete-note') + }, + toc: { + toc: $('.ui-toc'), + affix: $('.ui-affix-toc'), + label: $('.ui-toc-label'), + dropdown: $('.ui-toc-dropdown') + }, + area: { + edit: $('.ui-edit-area'), + view: $('.ui-view-area'), + codemirror: $('.ui-edit-area .CodeMirror'), + codemirrorScroll: $('.ui-edit-area .CodeMirror .CodeMirror-scroll'), + codemirrorSizer: $('.ui-edit-area .CodeMirror .CodeMirror-sizer'), + codemirrorSizerInner: $('.ui-edit-area .CodeMirror .CodeMirror-sizer > div'), + markdown: $('.ui-view-area .markdown-body'), + resize: { + handle: $('.ui-resizable-handle'), + syncToggle: $('.ui-sync-toggle') + } + }, + modal: { + snippetImportProjects: $('#snippetImportModalProjects'), + snippetImportSnippets: $('#snippetImportModalSnippets'), + revision: $('#revisionModal') + } +} + +// page actions var opts = { - lines: 11, // The number of lines to draw - length: 20, // The length of each line - width: 2, // The line thickness - radius: 30, // The radius of the inner circle - corners: 0, // Corner roundness (0..1) - rotate: 0, // The rotation offset - direction: 1, // 1: clockwise, -1: counterclockwise - color: '#000', // #rgb or #rrggbb or array of colors - speed: 1.1, // Rounds per second - trail: 60, // Afterglow percentage - shadow: false, // Whether to render a shadow - hwaccel: true, // Whether to use hardware acceleration - className: 'spinner', // The CSS class to assign to the spinner - zIndex: 2e9, // The z-index (defaults to 2000000000) - top: '50%', // Top position relative to parent - left: '50%' // Left position relative to parent -}; -var spinner = new Spinner(opts).spin(ui.spinner[0]); - -//idle + lines: 11, // The number of lines to draw + length: 20, // The length of each line + width: 2, // The line thickness + radius: 30, // The radius of the inner circle + corners: 0, // Corner roundness (0..1) + rotate: 0, // The rotation offset + direction: 1, // 1: clockwise, -1: counterclockwise + color: '#000', // #rgb or #rrggbb or array of colors + speed: 1.1, // Rounds per second + trail: 60, // Afterglow percentage + shadow: false, // Whether to render a shadow + hwaccel: true, // Whether to use hardware acceleration + className: 'spinner', // The CSS class to assign to the spinner + zIndex: 2e9, // The z-index (defaults to 2000000000) + top: '50%', // Top position relative to parent + left: '50%' // Left position relative to parent +} + +/* eslint-disable no-unused-vars */ +var spinner = new Spinner(opts).spin(ui.spinner[0]) +/* eslint-enable no-unused-vars */ + +// idle var idle = new Idle({ - onAway: function () { - idle.isAway = true; - emitUserStatus(); - updateOnlineStatus(); - }, - onAwayBack: function () { - idle.isAway = false; - emitUserStatus(); - updateOnlineStatus(); - setHaveUnreadChanges(false); - updateTitleReminder(); - }, - awayTimeout: idleTime -}); + onAway: function () { + idle.isAway = true + emitUserStatus() + updateOnlineStatus() + }, + onAwayBack: function () { + idle.isAway = false + emitUserStatus() + updateOnlineStatus() + setHaveUnreadChanges(false) + updateTitleReminder() + }, + awayTimeout: idleTime +}) ui.area.codemirror.on('touchstart', function () { - idle.onActive(); -}); + idle.onActive() +}) -var haveUnreadChanges = false; +var haveUnreadChanges = false -function setHaveUnreadChanges(bool) { - if (!loaded) return; - if (bool && (idle.isAway || Visibility.hidden())) { - haveUnreadChanges = true; - } else if (!bool && !idle.isAway && !Visibility.hidden()) { - haveUnreadChanges = false; - } +function setHaveUnreadChanges (bool) { + if (!window.loaded) return + if (bool && (idle.isAway || Visibility.hidden())) { + haveUnreadChanges = true + } else if (!bool && !idle.isAway && !Visibility.hidden()) { + haveUnreadChanges = false + } } -function updateTitleReminder() { - if (!loaded) return; - if (haveUnreadChanges) { - document.title = '• ' + renderTitle(ui.area.markdown); - } else { - document.title = renderTitle(ui.area.markdown); - } +function updateTitleReminder () { + if (!window.loaded) return + if (haveUnreadChanges) { + document.title = '• ' + renderTitle(ui.area.markdown) + } else { + document.title = renderTitle(ui.area.markdown) + } } -function setRefreshModal(status) { - $('#refreshModal').modal('show'); - $('#refreshModal').find('.modal-body > div').hide(); - $('#refreshModal').find('.' + status).show(); +function setRefreshModal (status) { + $('#refreshModal').modal('show') + $('#refreshModal').find('.modal-body > div').hide() + $('#refreshModal').find('.' + status).show() } -function setNeedRefresh() { - needRefresh = true; - editor.setOption('readOnly', true); - socket.disconnect(); - showStatus(statusType.offline); +function setNeedRefresh () { + window.needRefresh = true + editor.setOption('readOnly', true) + socket.disconnect() + showStatus(statusType.offline) } setloginStateChangeEvent(function () { - setRefreshModal('user-state-changed'); - setNeedRefresh(); -}); + setRefreshModal('user-state-changed') + setNeedRefresh() +}) -//visibility -var wasFocus = false; +// visibility +var wasFocus = false Visibility.change(function (e, state) { - var hidden = Visibility.hidden(); - if (hidden) { - if (editorHasFocus()) { - wasFocus = true; - editor.getInputField().blur(); - } - } else { - if (wasFocus) { - if (!visibleXS) { - editor.focus(); - editor.refresh(); - } - wasFocus = false; - } - setHaveUnreadChanges(false); - } - updateTitleReminder(); -}); + var hidden = Visibility.hidden() + if (hidden) { + if (editorHasFocus()) { + wasFocus = true + editor.getInputField().blur() + } + } else { + if (wasFocus) { + if (!window.visibleXS) { + editor.focus() + editor.refresh() + } + wasFocus = false + } + setHaveUnreadChanges(false) + } + updateTitleReminder() +}) -//when page ready +// when page ready $(document).ready(function () { - idle.checkAway(); - checkResponsive(); - //if in smaller screen, we don't need advanced scrollbar - var scrollbarStyle; - if (visibleXS) { - scrollbarStyle = 'native'; - } else { - scrollbarStyle = 'overlay'; - } - if (scrollbarStyle != editor.getOption('scrollbarStyle')) { - editor.setOption('scrollbarStyle', scrollbarStyle); - clearMap(); - } - checkEditorStyle(); + idle.checkAway() + checkResponsive() + // if in smaller screen, we don't need advanced scrollbar + var scrollbarStyle + if (window.visibleXS) { + scrollbarStyle = 'native' + } else { + scrollbarStyle = 'overlay' + } + if (scrollbarStyle !== editor.getOption('scrollbarStyle')) { + editor.setOption('scrollbarStyle', scrollbarStyle) + clearMap() + } + checkEditorStyle() /* we need this only on touch devices */ - if (isTouchDevice) { + if (window.isTouchDevice) { /* cache dom references */ - var $body = jQuery('body'); + var $body = jQuery('body') /* bind events */ - $(document) + $(document) .on('focus', 'textarea, input', function () { - $body.addClass('fixfixed'); + $body.addClass('fixfixed') }) .on('blur', 'textarea, input', function () { - $body.removeClass('fixfixed'); - }); - } - //showup - $().showUp('.navbar', { - upClass: 'navbar-hide', - downClass: 'navbar-show' - }); - //tooltip - $('[data-toggle="tooltip"]').tooltip(); + $body.removeClass('fixfixed') + }) + } + // showup + $().showUp('.navbar', { + upClass: 'navbar-hide', + downClass: 'navbar-show' + }) + // tooltip + $('[data-toggle="tooltip"]').tooltip() // shortcuts // allow on all tags - key.filter = function (e) { return true; }; - key('ctrl+alt+e', function (e) { - changeMode(modeType.edit); - }); - key('ctrl+alt+v', function (e) { - changeMode(modeType.view); - }); - key('ctrl+alt+b', function (e) { - changeMode(modeType.both); - }); + key.filter = function (e) { return true } + key('ctrl+alt+e', function (e) { + changeMode(modeType.edit) + }) + key('ctrl+alt+v', function (e) { + changeMode(modeType.view) + }) + key('ctrl+alt+b', function (e) { + changeMode(modeType.both) + }) // toggle-dropdown - $(document).on('click', '.toggle-dropdown .dropdown-menu', function (e) { - e.stopPropagation(); - }); -}); -//when page resize + $(document).on('click', '.toggle-dropdown .dropdown-menu', function (e) { + e.stopPropagation() + }) +}) +// when page resize $(window).resize(function () { - checkLayout(); - checkEditorStyle(); - checkTocStyle(); - checkCursorMenu(); - windowResize(); -}); -//when page unload + checkLayout() + checkEditorStyle() + checkTocStyle() + checkCursorMenu() + windowResize() +}) +// when page unload $(window).on('unload', function () { - //updateHistoryInner(); -}); + // updateHistoryInner(); +}) $(window).on('error', function () { - //setNeedRefresh(); -}); - -setupSyncAreas(ui.area.codemirrorScroll, ui.area.view, ui.area.markdown); + // setNeedRefresh(); +}) -function autoSyncscroll() { - if (editorHasFocus()) { - syncScrollToView(); +setupSyncAreas(ui.area.codemirrorScroll, ui.area.view, ui.area.markdown) + +function autoSyncscroll () { + if (editorHasFocus()) { + syncScrollToView() + } else { + syncScrollToEdit() + } +} + +var windowResizeDebounce = 200 +var windowResize = _.debounce(windowResizeInner, windowResizeDebounce) + +function windowResizeInner (callback) { + checkLayout() + checkResponsive() + checkEditorStyle() + checkTocStyle() + checkCursorMenu() + // refresh editor + if (window.loaded) { + if (editor.getOption('scrollbarStyle') === 'native') { + setTimeout(function () { + clearMap() + autoSyncscroll() + updateScrollspy() + if (callback && typeof callback === 'function') { callback() } + }, 1) } else { - syncScrollToEdit(); - } -} - -var windowResizeDebounce = 200; -var windowResize = _.debounce(windowResizeInner, windowResizeDebounce); - -function windowResizeInner(callback) { - checkLayout(); - checkResponsive(); - checkEditorStyle(); - checkTocStyle(); - checkCursorMenu(); - //refresh editor - if (loaded) { - if (editor.getOption('scrollbarStyle') === 'native') { - setTimeout(function () { - clearMap(); - autoSyncscroll(); - updateScrollspy(); - if (callback && typeof callback === 'function') - callback(); - }, 1); - } else { // force it load all docs at once to prevent scroll knob blink - editor.setOption('viewportMargin', Infinity); - setTimeout(function () { - clearMap(); - autoSyncscroll(); - editor.setOption('viewportMargin', viewportMargin); - //add or update user cursors - for (var i = 0; i < onlineUsers.length; i++) { - if (onlineUsers[i].id != personalInfo.id) - buildCursor(onlineUsers[i]); - } - updateScrollspy(); - if (callback && typeof callback === 'function') - callback(); - }, 1); + editor.setOption('viewportMargin', Infinity) + setTimeout(function () { + clearMap() + autoSyncscroll() + editor.setOption('viewportMargin', viewportMargin) + // add or update user cursors + for (var i = 0; i < window.onlineUsers.length; i++) { + if (window.onlineUsers[i].id !== window.personalInfo.id) { buildCursor(window.onlineUsers[i]) } } + updateScrollspy() + if (callback && typeof callback === 'function') { callback() } + }, 1) } + } } -function checkLayout() { - var navbarHieght = $('.navbar').outerHeight(); - $('body').css('padding-top', navbarHieght + 'px'); +function checkLayout () { + var navbarHieght = $('.navbar').outerHeight() + $('body').css('padding-top', navbarHieght + 'px') } -function editorHasFocus() { - return $(editor.getInputField()).is(":focus"); +function editorHasFocus () { + return $(editor.getInputField()).is(':focus') } -//768-792px have a gap -function checkResponsive() { - visibleXS = $(".visible-xs").is(":visible"); - visibleSM = $(".visible-sm").is(":visible"); - visibleMD = $(".visible-md").is(":visible"); - visibleLG = $(".visible-lg").is(":visible"); +// 768-792px have a gap +function checkResponsive () { + window.visibleXS = $('.visible-xs').is(':visible') + window.visibleSM = $('.visible-sm').is(':visible') + window.visibleMD = $('.visible-md').is(':visible') + window.visibleLG = $('.visible-lg').is(':visible') - if (visibleXS && currentMode == modeType.both) - if (editorHasFocus()) - changeMode(modeType.edit); - else - changeMode(modeType.view); + if (window.visibleXS && window.currentMode === modeType.both) { + if (editorHasFocus()) { changeMode(modeType.edit) } else { changeMode(modeType.view) } + } - emitUserStatus(); + emitUserStatus() } -var lastEditorWidth = 0; -var previousFocusOnEditor = null; +var lastEditorWidth = 0 +var previousFocusOnEditor = null -function checkEditorStyle() { - var desireHeight = statusBar ? (ui.area.edit.height() - statusBar.outerHeight()) : ui.area.edit.height(); +function checkEditorStyle () { + var desireHeight = statusBar ? (ui.area.edit.height() - statusBar.outerHeight()) : ui.area.edit.height() // set editor height and min height based on scrollbar style and mode - var scrollbarStyle = editor.getOption('scrollbarStyle'); - if (scrollbarStyle == 'overlay' || currentMode == modeType.both) { - ui.area.codemirrorScroll.css('height', desireHeight + 'px'); - ui.area.codemirrorScroll.css('min-height', ''); - checkEditorScrollbar(); - } else if (scrollbarStyle == 'native') { - ui.area.codemirrorScroll.css('height', ''); - ui.area.codemirrorScroll.css('min-height', desireHeight + 'px'); - } + var scrollbarStyle = editor.getOption('scrollbarStyle') + if (scrollbarStyle === 'overlay' || window.currentMode === modeType.both) { + ui.area.codemirrorScroll.css('height', desireHeight + 'px') + ui.area.codemirrorScroll.css('min-height', '') + checkEditorScrollbar() + } else if (scrollbarStyle === 'native') { + ui.area.codemirrorScroll.css('height', '') + ui.area.codemirrorScroll.css('min-height', desireHeight + 'px') + } // workaround editor will have wrong doc height when editor height changed - editor.setSize(null, ui.area.edit.height()); - //make editor resizable - if (!ui.area.resize.handle.length) { - ui.area.edit.resizable({ - handles: 'e', - maxWidth: $(window).width() * 0.7, - minWidth: $(window).width() * 0.2, - create: function (e, ui) { - $(this).parent().on('resize', function (e) { - e.stopPropagation(); - }); - }, - start: function (e) { - editor.setOption('viewportMargin', Infinity); - }, - resize: function (e) { - ui.area.resize.syncToggle.stop(true, true).show(); - checkTocStyle(); - }, - stop: function (e) { - lastEditorWidth = ui.area.edit.width(); + editor.setSize(null, ui.area.edit.height()) + // make editor resizable + if (!ui.area.resize.handle.length) { + ui.area.edit.resizable({ + handles: 'e', + maxWidth: $(window).width() * 0.7, + minWidth: $(window).width() * 0.2, + create: function (e, ui) { + $(this).parent().on('resize', function (e) { + e.stopPropagation() + }) + }, + start: function (e) { + editor.setOption('viewportMargin', Infinity) + }, + resize: function (e) { + ui.area.resize.syncToggle.stop(true, true).show() + checkTocStyle() + }, + stop: function (e) { + lastEditorWidth = ui.area.edit.width() // workaround that scroll event bindings - preventSyncScrollToView = 2; - preventSyncScrollToEdit = true; - editor.setOption('viewportMargin', viewportMargin); - if (editorHasFocus()) { - windowResizeInner(function () { - ui.area.codemirrorScroll.scroll(); - }); - } else { - windowResizeInner(function () { - ui.area.view.scroll(); - }); - } - checkEditorScrollbar(); - } - }); - ui.area.resize.handle = $('.ui-resizable-handle'); - } - if (!ui.area.resize.syncToggle.length) { - ui.area.resize.syncToggle = $('<button class="btn btn-lg btn-default ui-sync-toggle" title="Toggle sync scrolling"><i class="fa fa-link fa-fw"></i></button>'); - ui.area.resize.syncToggle.hover(function () { - previousFocusOnEditor = editorHasFocus(); - }, function () { - previousFocusOnEditor = null; - }); - ui.area.resize.syncToggle.click(function () { - syncscroll = !syncscroll; - checkSyncToggle(); - }); - ui.area.resize.handle.append(ui.area.resize.syncToggle); - ui.area.resize.syncToggle.hide(); - ui.area.resize.handle.hover(function () { - ui.area.resize.syncToggle.stop(true, true).delay(200).fadeIn(100); - }, function () { - ui.area.resize.syncToggle.stop(true, true).delay(300).fadeOut(300); - }); - } -} - -function checkSyncToggle() { - if (syncscroll) { - if (previousFocusOnEditor) { - preventSyncScrollToView = false; - syncScrollToView(); + window.preventSyncScrollToView = 2 + window.preventSyncScrollToEdit = true + editor.setOption('viewportMargin', viewportMargin) + if (editorHasFocus()) { + windowResizeInner(function () { + ui.area.codemirrorScroll.scroll() + }) } else { - preventSyncScrollToEdit = false; - syncScrollToEdit(); + windowResizeInner(function () { + ui.area.view.scroll() + }) } - ui.area.resize.syncToggle.find('i').removeClass('fa-unlink').addClass('fa-link'); + checkEditorScrollbar() + } + }) + ui.area.resize.handle = $('.ui-resizable-handle') + } + if (!ui.area.resize.syncToggle.length) { + ui.area.resize.syncToggle = $('<button class="btn btn-lg btn-default ui-sync-toggle" title="Toggle sync scrolling"><i class="fa fa-link fa-fw"></i></button>') + ui.area.resize.syncToggle.hover(function () { + previousFocusOnEditor = editorHasFocus() + }, function () { + previousFocusOnEditor = null + }) + ui.area.resize.syncToggle.click(function () { + window.syncscroll = !window.syncscroll + checkSyncToggle() + }) + ui.area.resize.handle.append(ui.area.resize.syncToggle) + ui.area.resize.syncToggle.hide() + ui.area.resize.handle.hover(function () { + ui.area.resize.syncToggle.stop(true, true).delay(200).fadeIn(100) + }, function () { + ui.area.resize.syncToggle.stop(true, true).delay(300).fadeOut(300) + }) + } +} + +function checkSyncToggle () { + if (window.syncscroll) { + if (previousFocusOnEditor) { + window.preventSyncScrollToView = false + syncScrollToView() } else { - ui.area.resize.syncToggle.find('i').removeClass('fa-link').addClass('fa-unlink'); + window.preventSyncScrollToEdit = false + syncScrollToEdit() } + ui.area.resize.syncToggle.find('i').removeClass('fa-unlink').addClass('fa-link') + } else { + ui.area.resize.syncToggle.find('i').removeClass('fa-link').addClass('fa-unlink') + } } var checkEditorScrollbar = _.debounce(function () { - editor.operation(checkEditorScrollbarInner); -}, 50); + editor.operation(checkEditorScrollbarInner) +}, 50) -function checkEditorScrollbarInner() { +function checkEditorScrollbarInner () { // workaround simple scroll bar knob // will get wrong position when editor height changed - var scrollInfo = editor.getScrollInfo(); - editor.scrollTo(null, scrollInfo.top - 1); - editor.scrollTo(null, scrollInfo.top); -} - -function checkTocStyle() { - //toc right - var paddingRight = parseFloat(ui.area.markdown.css('padding-right')); - var right = ($(window).width() - (ui.area.markdown.offset().left + ui.area.markdown.outerWidth() - paddingRight)); - ui.toc.toc.css('right', right + 'px'); - //affix toc left - var newbool; - var rightMargin = (ui.area.markdown.parent().outerWidth() - ui.area.markdown.outerWidth()) / 2; - //for ipad or wider device - if (rightMargin >= 133) { - newbool = true; - var affixLeftMargin = (ui.toc.affix.outerWidth() - ui.toc.affix.width()) / 2; - var left = ui.area.markdown.offset().left + ui.area.markdown.outerWidth() - affixLeftMargin; - ui.toc.affix.css('left', left + 'px'); - ui.toc.affix.css('width', rightMargin + 'px'); - } else { - newbool = false; - } - //toc scrollspy - ui.toc.toc.removeClass('scrollspy-body, scrollspy-view'); - ui.toc.affix.removeClass('scrollspy-body, scrollspy-view'); - if (currentMode == modeType.both) { - ui.toc.toc.addClass('scrollspy-view'); - ui.toc.affix.addClass('scrollspy-view'); - } else if (currentMode != modeType.both && !newbool) { - ui.toc.toc.addClass('scrollspy-body'); - ui.toc.affix.addClass('scrollspy-body'); - } else { - ui.toc.toc.addClass('scrollspy-view'); - ui.toc.affix.addClass('scrollspy-body'); - } - if (newbool != enoughForAffixToc) { - enoughForAffixToc = newbool; - generateScrollspy(); - } -} - -function showStatus(type, num) { - currentStatus = type; - var shortStatus = ui.toolbar.shortStatus; - var status = ui.toolbar.status; - var label = $('<span class="label"></span>'); - var fa = $('<i class="fa"></i>'); - var msg = ""; - var shortMsg = ""; - - shortStatus.html(""); - status.html(""); - - switch (currentStatus) { - case statusType.connected: - label.addClass(statusType.connected.label); - fa.addClass(statusType.connected.fa); - msg = statusType.connected.msg; - break; - case statusType.online: - label.addClass(statusType.online.label); - fa.addClass(statusType.online.fa); - shortMsg = num; - msg = num + " " + statusType.online.msg; - break; - case statusType.offline: - label.addClass(statusType.offline.label); - fa.addClass(statusType.offline.fa); - msg = statusType.offline.msg; - break; - } - - label.append(fa); - var shortLabel = label.clone(); - - shortLabel.append(" " + shortMsg); - shortStatus.append(shortLabel); - - label.append(" " + msg); - status.append(label); -} - -function toggleMode() { - switch (currentMode) { - case modeType.edit: - changeMode(modeType.view); - break; - case modeType.view: - changeMode(modeType.edit); - break; - case modeType.both: - changeMode(modeType.view); - break; - } -} - -var lastMode = null; - -function changeMode(type) { + var scrollInfo = editor.getScrollInfo() + editor.scrollTo(null, scrollInfo.top - 1) + editor.scrollTo(null, scrollInfo.top) +} + +function checkTocStyle () { + // toc right + var paddingRight = parseFloat(ui.area.markdown.css('padding-right')) + var right = ($(window).width() - (ui.area.markdown.offset().left + ui.area.markdown.outerWidth() - paddingRight)) + ui.toc.toc.css('right', right + 'px') + // affix toc left + var newbool + var rightMargin = (ui.area.markdown.parent().outerWidth() - ui.area.markdown.outerWidth()) / 2 + // for ipad or wider device + if (rightMargin >= 133) { + newbool = true + var affixLeftMargin = (ui.toc.affix.outerWidth() - ui.toc.affix.width()) / 2 + var left = ui.area.markdown.offset().left + ui.area.markdown.outerWidth() - affixLeftMargin + ui.toc.affix.css('left', left + 'px') + ui.toc.affix.css('width', rightMargin + 'px') + } else { + newbool = false + } + // toc scrollspy + ui.toc.toc.removeClass('scrollspy-body, scrollspy-view') + ui.toc.affix.removeClass('scrollspy-body, scrollspy-view') + if (window.currentMode === modeType.both) { + ui.toc.toc.addClass('scrollspy-view') + ui.toc.affix.addClass('scrollspy-view') + } else if (window.currentMode !== modeType.both && !newbool) { + ui.toc.toc.addClass('scrollspy-body') + ui.toc.affix.addClass('scrollspy-body') + } else { + ui.toc.toc.addClass('scrollspy-view') + ui.toc.affix.addClass('scrollspy-body') + } + if (newbool !== enoughForAffixToc) { + enoughForAffixToc = newbool + generateScrollspy() + } +} + +function showStatus (type, num) { + window.currentStatus = type + var shortStatus = ui.toolbar.shortStatus + var status = ui.toolbar.status + var label = $('<span class="label"></span>') + var fa = $('<i class="fa"></i>') + var msg = '' + var shortMsg = '' + + shortStatus.html('') + status.html('') + + switch (window.currentStatus) { + case statusType.connected: + label.addClass(statusType.connected.label) + fa.addClass(statusType.connected.fa) + msg = statusType.connected.msg + break + case statusType.online: + label.addClass(statusType.online.label) + fa.addClass(statusType.online.fa) + shortMsg = num + msg = num + ' ' + statusType.online.msg + break + case statusType.offline: + label.addClass(statusType.offline.label) + fa.addClass(statusType.offline.fa) + msg = statusType.offline.msg + break + } + + label.append(fa) + var shortLabel = label.clone() + + shortLabel.append(' ' + shortMsg) + shortStatus.append(shortLabel) + + label.append(' ' + msg) + status.append(label) +} + +function toggleMode () { + switch (window.currentMode) { + case modeType.edit: + changeMode(modeType.view) + break + case modeType.view: + changeMode(modeType.edit) + break + case modeType.both: + changeMode(modeType.view) + break + } +} + +var lastMode = null + +function changeMode (type) { // lock navbar to prevent it hide after changeMode - lockNavbar(); - saveInfo(); - if (type) { - lastMode = currentMode; - currentMode = type; - } - var responsiveClass = "col-lg-6 col-md-6 col-sm-6"; - var scrollClass = "ui-scrollable"; - ui.area.codemirror.removeClass(scrollClass); - ui.area.edit.removeClass(responsiveClass); - ui.area.view.removeClass(scrollClass); - ui.area.view.removeClass(responsiveClass); - switch (currentMode) { - case modeType.edit: - ui.area.edit.show(); - ui.area.view.hide(); - if (!editShown) { - editor.refresh(); - editShown = true; - } - break; - case modeType.view: - ui.area.edit.hide(); - ui.area.view.show(); - break; - case modeType.both: - ui.area.codemirror.addClass(scrollClass); - ui.area.edit.addClass(responsiveClass).show(); - ui.area.view.addClass(scrollClass); - ui.area.view.show(); - break; - } + lockNavbar() + saveInfo() + if (type) { + lastMode = window.currentMode + window.currentMode = type + } + var responsiveClass = 'col-lg-6 col-md-6 col-sm-6' + var scrollClass = 'ui-scrollable' + ui.area.codemirror.removeClass(scrollClass) + ui.area.edit.removeClass(responsiveClass) + ui.area.view.removeClass(scrollClass) + ui.area.view.removeClass(responsiveClass) + switch (window.currentMode) { + case modeType.edit: + ui.area.edit.show() + ui.area.view.hide() + if (!window.editShown) { + editor.refresh() + window.editShown = true + } + break + case modeType.view: + ui.area.edit.hide() + ui.area.view.show() + break + case modeType.both: + ui.area.codemirror.addClass(scrollClass) + ui.area.edit.addClass(responsiveClass).show() + ui.area.view.addClass(scrollClass) + ui.area.view.show() + break + } // save mode to url - if (history.replaceState && loaded) history.replaceState(null, "", serverurl + '/' + noteid + '?' + currentMode.name); - if (currentMode == modeType.view) { - editor.getInputField().blur(); - } - if (currentMode == modeType.edit || currentMode == modeType.both) { - ui.toolbar.uploadImage.fadeIn(); - //add and update status bar - if (!statusBar) { - addStatusBar(); - updateStatusBar(); - } - //work around foldGutter might not init properly - editor.setOption('foldGutter', false); - editor.setOption('foldGutter', true); - } else { - ui.toolbar.uploadImage.fadeOut(); - } - if (currentMode != modeType.edit) { - $(document.body).css('background-color', 'white'); - updateView(); - } else { - $(document.body).css('background-color', ui.area.codemirror.css('background-color')); - } - //check resizable editor style - if (currentMode == modeType.both) { - if (lastEditorWidth > 0) - ui.area.edit.css('width', lastEditorWidth + 'px'); - else - ui.area.edit.css('width', ''); - ui.area.resize.handle.show(); - } else { - ui.area.edit.css('width', ''); - ui.area.resize.handle.hide(); - } - - windowResizeInner(); - - restoreInfo(); - - if (lastMode == modeType.view && currentMode == modeType.both) { - preventSyncScrollToView = 2; - syncScrollToEdit(null, true); - } - - if (lastMode == modeType.edit && currentMode == modeType.both) { - preventSyncScrollToEdit = 2; - syncScrollToView(null, true); - } - - if (lastMode == modeType.both && currentMode != modeType.both) { - preventSyncScrollToView = false; - preventSyncScrollToEdit = false; - } - - if (lastMode != modeType.edit && currentMode == modeType.edit) { - editor.refresh(); - } - - $(document.body).scrollspy('refresh'); - ui.area.view.scrollspy('refresh'); - - ui.toolbar.both.removeClass("active"); - ui.toolbar.edit.removeClass("active"); - ui.toolbar.view.removeClass("active"); - var modeIcon = ui.toolbar.mode.find('i'); - modeIcon.removeClass('fa-pencil').removeClass('fa-eye'); - if (ui.area.edit.is(":visible") && ui.area.view.is(":visible")) { //both - ui.toolbar.both.addClass("active"); - modeIcon.addClass('fa-eye'); - } else if (ui.area.edit.is(":visible")) { //edit - ui.toolbar.edit.addClass("active"); - modeIcon.addClass('fa-eye'); - } else if (ui.area.view.is(":visible")) { //view - ui.toolbar.view.addClass("active"); - modeIcon.addClass('fa-pencil'); - } - unlockNavbar(); -} - -function lockNavbar() { - $('.navbar').addClass('locked'); + if (history.replaceState && window.loaded) history.replaceState(null, '', serverurl + '/' + noteid + '?' + window.currentMode.name) + if (window.currentMode === modeType.view) { + editor.getInputField().blur() + } + if (window.currentMode === modeType.edit || window.currentMode === modeType.both) { + ui.toolbar.uploadImage.fadeIn() + // add and update status bar + if (!statusBar) { + addStatusBar() + updateStatusBar() + } + // work around foldGutter might not init properly + editor.setOption('foldGutter', false) + editor.setOption('foldGutter', true) + } else { + ui.toolbar.uploadImage.fadeOut() + } + if (window.currentMode !== modeType.edit) { + $(document.body).css('background-color', 'white') + updateView() + } else { + $(document.body).css('background-color', ui.area.codemirror.css('background-color')) + } + // check resizable editor style + if (window.currentMode === modeType.both) { + if (lastEditorWidth > 0) { ui.area.edit.css('width', lastEditorWidth + 'px') } else { ui.area.edit.css('width', '') } + ui.area.resize.handle.show() + } else { + ui.area.edit.css('width', '') + ui.area.resize.handle.hide() + } + + windowResizeInner() + + restoreInfo() + + if (lastMode === modeType.view && window.currentMode === modeType.both) { + window.preventSyncScrollToView = 2 + syncScrollToEdit(null, true) + } + + if (lastMode === modeType.edit && window.currentMode === modeType.both) { + window.preventSyncScrollToEdit = 2 + syncScrollToView(null, true) + } + + if (lastMode === modeType.both && window.currentMode !== modeType.both) { + window.preventSyncScrollToView = false + window.preventSyncScrollToEdit = false + } + + if (lastMode !== modeType.edit && window.currentMode === modeType.edit) { + editor.refresh() + } + + $(document.body).scrollspy('refresh') + ui.area.view.scrollspy('refresh') + + ui.toolbar.both.removeClass('active') + ui.toolbar.edit.removeClass('active') + ui.toolbar.view.removeClass('active') + var modeIcon = ui.toolbar.mode.find('i') + modeIcon.removeClass('fa-pencil').removeClass('fa-eye') + if (ui.area.edit.is(':visible') && ui.area.view.is(':visible')) { // both + ui.toolbar.both.addClass('active') + modeIcon.addClass('fa-eye') + } else if (ui.area.edit.is(':visible')) { // edit + ui.toolbar.edit.addClass('active') + modeIcon.addClass('fa-eye') + } else if (ui.area.view.is(':visible')) { // view + ui.toolbar.view.addClass('active') + modeIcon.addClass('fa-pencil') + } + unlockNavbar() +} + +function lockNavbar () { + $('.navbar').addClass('locked') } var unlockNavbar = _.debounce(function () { - $('.navbar').removeClass('locked'); -}, 200); - -function closestIndex(arr, closestTo) { - var closest = Math.max.apply(null, arr); //Get the highest number in arr in case it match nothing. - var index = 0; - for (var i = 0; i < arr.length; i++) { //Loop the array - if (arr[i] >= closestTo && arr[i] < closest) { - closest = arr[i]; //Check if it's higher than your number, but lower than your closest value - index = i; - } - } - return index; // return the value -} + $('.navbar').removeClass('locked') +}, 200) -function showMessageModal(title, header, href, text, success) { - var modal = $('.message-modal'); - modal.find('.modal-title').html(title); - modal.find('.modal-body h5').html(header); - if (href) - modal.find('.modal-body a').attr('href', href).text(text); - else - modal.find('.modal-body a').removeAttr('href').text(text); - modal.find('.modal-footer button').removeClass('btn-default btn-success btn-danger') - if (success) - modal.find('.modal-footer button').addClass('btn-success'); - else - modal.find('.modal-footer button').addClass('btn-danger'); - modal.modal('show'); +function showMessageModal (title, header, href, text, success) { + var modal = $('.message-modal') + modal.find('.modal-title').html(title) + modal.find('.modal-body h5').html(header) + if (href) { modal.find('.modal-body a').attr('href', href).text(text) } else { modal.find('.modal-body a').removeAttr('href').text(text) } + modal.find('.modal-footer button').removeClass('btn-default btn-success btn-danger') + if (success) { modal.find('.modal-footer button').addClass('btn-success') } else { modal.find('.modal-footer button').addClass('btn-danger') } + modal.modal('show') } // check if dropbox app key is set and load scripts if (DROPBOX_APP_KEY) { - $('<script>') + $('<script>') .attr('type', 'text/javascript') .attr('src', 'https://www.dropbox.com/static/api/2/dropins.js') .attr('id', 'dropboxjs') .attr('data-app-key', DROPBOX_APP_KEY) .prop('async', true) .prop('defer', true) - .appendTo('body'); + .appendTo('body') } else { - ui.toolbar.import.dropbox.hide(); - ui.toolbar.export.dropbox.hide(); + ui.toolbar.import.dropbox.hide() + ui.toolbar.export.dropbox.hide() } // check if google api key and client id are set and load scripts if (GOOGLE_API_KEY && GOOGLE_CLIENT_ID) { - $('<script>') + $('<script>') .attr('type', 'text/javascript') .attr('src', 'https://www.google.com/jsapi?callback=onGoogleAPILoaded') .prop('async', true) .prop('defer', true) - .appendTo('body'); + .appendTo('body') } else { - ui.toolbar.import.googleDrive.hide(); - ui.toolbar.export.googleDrive.hide(); + ui.toolbar.import.googleDrive.hide() + ui.toolbar.export.googleDrive.hide() } -function onGoogleAPILoaded() { - $('<script>') +function onGoogleAPILoaded () { + $('<script>') .attr('type', 'text/javascript') .attr('src', 'https://apis.google.com/js/client:plusone.js?onload=onGoogleClientLoaded') .prop('async', true) .prop('defer', true) - .appendTo('body'); + .appendTo('body') } -window.onGoogleAPILoaded = onGoogleAPILoaded; +window.onGoogleAPILoaded = onGoogleAPILoaded -//button actions -//share -ui.toolbar.publish.attr("href", noteurl + "/publish"); +// button actions +// share +ui.toolbar.publish.attr('href', noteurl + '/publish') // extra -//slide -ui.toolbar.extra.slide.attr("href", noteurl + "/slide"); -//download -//markdown +// slide +ui.toolbar.extra.slide.attr('href', noteurl + '/slide') +// download +// markdown ui.toolbar.download.markdown.click(function (e) { - e.preventDefault(); - e.stopPropagation(); - var filename = renderFilename(ui.area.markdown) + '.md'; - var markdown = editor.getValue(); - var blob = new Blob([markdown], { - type: "text/markdown;charset=utf-8" - }); - saveAs(blob, filename, true); -}); -//html + e.preventDefault() + e.stopPropagation() + var filename = renderFilename(ui.area.markdown) + '.md' + var markdown = editor.getValue() + var blob = new Blob([markdown], { + type: 'text/markdown;charset=utf-8' + }) + saveAs(blob, filename, true) +}) +// html ui.toolbar.download.html.click(function (e) { - e.preventDefault(); - e.stopPropagation(); - exportToHTML(ui.area.markdown); -}); + e.preventDefault() + e.stopPropagation() + exportToHTML(ui.area.markdown) +}) // raw html ui.toolbar.download.rawhtml.click(function (e) { - e.preventDefault(); - e.stopPropagation(); - exportToRawHTML(ui.area.markdown); -}); -//pdf -ui.toolbar.download.pdf.attr("download", "").attr("href", noteurl + "/pdf"); -//export to dropbox + e.preventDefault() + e.stopPropagation() + exportToRawHTML(ui.area.markdown) +}) +// pdf +ui.toolbar.download.pdf.attr('download', '').attr('href', noteurl + '/pdf') +// export to dropbox ui.toolbar.export.dropbox.click(function () { - var filename = renderFilename(ui.area.markdown) + '.md'; - var options = { - files: [ - { - 'url': noteurl + "/download", - 'filename': filename - } - ], - error: function (errorMessage) { - console.error(errorMessage); - } - }; - Dropbox.save(options); -}); -function uploadToGoogleDrive(accessToken) { - ui.spinner.show(); - var filename = renderFilename(ui.area.markdown) + '.md'; - var markdown = editor.getValue(); - var blob = new Blob([markdown], { - type: "text/markdown;charset=utf-8" - }); - blob.name = filename; - var uploader = new MediaUploader({ - file: blob, - token: accessToken, - onComplete: function (data) { - data = JSON.parse(data); - showMessageModal('<i class="fa fa-cloud-upload"></i> Export to Google Drive', 'Export Complete!', data.alternateLink, 'Click here to view your file', true); - ui.spinner.hide(); - }, - onError: function (data) { - var modal = $('.export-modal'); - showMessageModal('<i class="fa fa-cloud-upload"></i> Export to Google Drive', 'Export Error :(', '', data, false); - ui.spinner.hide(); - } - }); - uploader.upload(); -} -function googleApiAuth(immediate, callback) { - gapi.auth.authorize( - { - 'client_id': GOOGLE_CLIENT_ID, - 'scope': 'https://www.googleapis.com/auth/drive.file', - 'immediate': immediate - }, callback ? callback : function () { }); -} -function onGoogleClientLoaded() { - googleApiAuth(true); - buildImportFromGoogleDrive(); -} -window.onGoogleClientLoaded = onGoogleClientLoaded; + var filename = renderFilename(ui.area.markdown) + '.md' + var options = { + files: [ + { + 'url': noteurl + '/download', + 'filename': filename + } + ], + error: function (errorMessage) { + console.error(errorMessage) + } + } + Dropbox.save(options) +}) +function uploadToGoogleDrive (accessToken) { + ui.spinner.show() + var filename = renderFilename(ui.area.markdown) + '.md' + var markdown = editor.getValue() + var blob = new Blob([markdown], { + type: 'text/markdown;charset=utf-8' + }) + blob.name = filename + var uploader = new MediaUploader({ + file: blob, + token: accessToken, + onComplete: function (data) { + data = JSON.parse(data) + showMessageModal('<i class="fa fa-cloud-upload"></i> Export to Google Drive', 'Export Complete!', data.alternateLink, 'Click here to view your file', true) + ui.spinner.hide() + }, + onError: function (data) { + showMessageModal('<i class="fa fa-cloud-upload"></i> Export to Google Drive', 'Export Error :(', '', data, false) + ui.spinner.hide() + } + }) + uploader.upload() +} +function googleApiAuth (immediate, callback) { + gapi.auth.authorize( + { + 'client_id': GOOGLE_CLIENT_ID, + 'scope': 'https://www.googleapis.com/auth/drive.file', + 'immediate': immediate + }, callback || function () { }) +} +function onGoogleClientLoaded () { + googleApiAuth(true) + buildImportFromGoogleDrive() +} +window.onGoogleClientLoaded = onGoogleClientLoaded // export to google drive ui.toolbar.export.googleDrive.click(function (e) { - var token = gapi.auth.getToken(); - if (token) { - uploadToGoogleDrive(token.access_token); - } else { - googleApiAuth(false, function (result) { - uploadToGoogleDrive(result.access_token); - }); - } -}); -//export to gist -ui.toolbar.export.gist.attr("href", noteurl + "/gist"); -//export to snippet -ui.toolbar.export.snippet.click(function() { - ui.spinner.show(); - $.get(serverurl + '/auth/gitlab/callback/' + noteid + '/projects') + var token = gapi.auth.getToken() + if (token) { + uploadToGoogleDrive(token.access_token) + } else { + googleApiAuth(false, function (result) { + uploadToGoogleDrive(result.access_token) + }) + } +}) +// export to gist +ui.toolbar.export.gist.attr('href', noteurl + '/gist') +// export to snippet +ui.toolbar.export.snippet.click(function () { + ui.spinner.show() + $.get(serverurl + '/auth/gitlab/callback/' + noteid + '/projects') .done(function (data) { - $("#snippetExportModalAccessToken").val(data.accesstoken); - $("#snippetExportModalBaseURL").val(data.baseURL); - $("#snippetExportModalLoading").hide(); - $("#snippetExportModal").modal('toggle'); - $("#snippetExportModalProjects").find('option').remove().end().append('<option value="init" selected="selected" disabled="disabled">Select From Available Projects</option>'); - if (data.projects) { - data.projects.sort(function(a,b) { - return (a.path_with_namespace < b.path_with_namespace) ? -1 : ((a.path_with_namespace > b.path_with_namespace) ? 1 : 0); - }); - data.projects.forEach(function(project) { - if (!project.snippets_enabled - || (project.permissions.project_access === null && project.permissions.group_access === null) - || (project.permissions.project_access !== null && project.permissions.project_access.access_level < 20)) - { - return; - } - $('<option>').val(project.id).text(project.path_with_namespace).appendTo("#snippetExportModalProjects"); - }); - $("#snippetExportModalProjects").prop('disabled', false); - } - $("#snippetExportModalLoading").hide(); + $('#snippetExportModalAccessToken').val(data.accesstoken) + $('#snippetExportModalBaseURL').val(data.baseURL) + $('#snippetExportModalLoading').hide() + $('#snippetExportModal').modal('toggle') + $('#snippetExportModalProjects').find('option').remove().end().append('<option value="init" selected="selected" disabled="disabled">Select From Available Projects</option>') + if (data.projects) { + data.projects.sort(function (a, b) { + return (a.path_with_namespace < b.path_with_namespace) ? -1 : ((a.path_with_namespace > b.path_with_namespace) ? 1 : 0) + }) + data.projects.forEach(function (project) { + if (!project.snippets_enabled || + (project.permissions.project_access === null && project.permissions.group_access === null) || + (project.permissions.project_access !== null && project.permissions.project_access.access_level < 20)) { + return + } + $('<option>').val(project.id).text(project.path_with_namespace).appendTo('#snippetExportModalProjects') + }) + $('#snippetExportModalProjects').prop('disabled', false) + } + $('#snippetExportModalLoading').hide() }) .fail(function (data) { - showMessageModal('<i class="fa fa-gitlab"></i> Import from Snippet', 'Unable to fetch gitlab parameters :(', '', '', false); + showMessageModal('<i class="fa fa-gitlab"></i> Import from Snippet', 'Unable to fetch gitlab parameters :(', '', '', false) }) .always(function () { - ui.spinner.hide(); - }); -}); -//import from dropbox + ui.spinner.hide() + }) +}) +// import from dropbox ui.toolbar.import.dropbox.click(function () { - var options = { - success: function (files) { - ui.spinner.show(); - var url = files[0].link; - importFromUrl(url); - }, - linkType: "direct", - multiselect: false, - extensions: ['.md', '.html'] - }; - Dropbox.choose(options); -}); + var options = { + success: function (files) { + ui.spinner.show() + var url = files[0].link + importFromUrl(url) + }, + linkType: 'direct', + multiselect: false, + extensions: ['.md', '.html'] + } + Dropbox.choose(options) +}) // import from google drive -var picker = null; -function buildImportFromGoogleDrive() { - picker = new FilePicker({ - apiKey: GOOGLE_API_KEY, - clientId: GOOGLE_CLIENT_ID, - buttonEl: ui.toolbar.import.googleDrive, - onSelect: function (file) { - if (file.downloadUrl) { - ui.spinner.show(); - var accessToken = gapi.auth.getToken().access_token; - $.ajax({ - type: 'GET', - beforeSend: function (request) { - request.setRequestHeader('Authorization', 'Bearer ' + accessToken); - }, - url: file.downloadUrl, - success: function (data) { - if (file.fileExtension == 'html') - parseToEditor(data); - else - replaceAll(data); - }, - error: function (data) { - showMessageModal('<i class="fa fa-cloud-download"></i> Import from Google Drive', 'Import failed :(', '', data, false); - }, - complete: function () { - ui.spinner.hide(); - } - }); - } - } - }); +function buildImportFromGoogleDrive () { + FilePicker({ + apiKey: GOOGLE_API_KEY, + clientId: GOOGLE_CLIENT_ID, + buttonEl: ui.toolbar.import.googleDrive, + onSelect: function (file) { + if (file.downloadUrl) { + ui.spinner.show() + var accessToken = gapi.auth.getToken().access_token + $.ajax({ + type: 'GET', + beforeSend: function (request) { + request.setRequestHeader('Authorization', 'Bearer ' + accessToken) + }, + url: file.downloadUrl, + success: function (data) { + if (file.fileExtension === 'html') { parseToEditor(data) } else { replaceAll(data) } + }, + error: function (data) { + showMessageModal('<i class="fa fa-cloud-download"></i> Import from Google Drive', 'Import failed :(', '', data, false) + }, + complete: function () { + ui.spinner.hide() + } + }) + } + } + }) } -//import from gist +// import from gist ui.toolbar.import.gist.click(function () { - //na -}); -//import from snippet + // na +}) +// import from snippet ui.toolbar.import.snippet.click(function () { - ui.spinner.show(); - $.get(serverurl + '/auth/gitlab/callback/' + noteid + '/projects') + ui.spinner.show() + $.get(serverurl + '/auth/gitlab/callback/' + noteid + '/projects') .done(function (data) { - $("#snippetImportModalAccessToken").val(data.accesstoken); - $("#snippetImportModalBaseURL").val(data.baseURL); - $("#snippetImportModalContent").prop('disabled', false); - $("#snippetImportModalConfirm").prop('disabled', false); - $("#snippetImportModalLoading").hide(); - $("#snippetImportModal").modal('toggle'); - $("#snippetImportModalProjects").find('option').remove().end().append('<option value="init" selected="selected" disabled="disabled">Select From Available Projects</option>'); - if (data.projects) { - data.projects.sort(function(a,b) { - return (a.path_with_namespace < b.path_with_namespace) ? -1 : ((a.path_with_namespace > b.path_with_namespace) ? 1 : 0); - }); - data.projects.forEach(function(project) { - if (!project.snippets_enabled - || (project.permissions.project_access === null && project.permissions.group_access === null) - || (project.permissions.project_access !== null && project.permissions.project_access.access_level < 20)) - { - return; - } - $('<option>').val(project.id).text(project.path_with_namespace).appendTo("#snippetImportModalProjects"); - }); - $("#snippetImportModalProjects").prop('disabled', false); - } - $("#snippetImportModalLoading").hide(); + $('#snippetImportModalAccessToken').val(data.accesstoken) + $('#snippetImportModalBaseURL').val(data.baseURL) + $('#snippetImportModalContent').prop('disabled', false) + $('#snippetImportModalConfirm').prop('disabled', false) + $('#snippetImportModalLoading').hide() + $('#snippetImportModal').modal('toggle') + $('#snippetImportModalProjects').find('option').remove().end().append('<option value="init" selected="selected" disabled="disabled">Select From Available Projects</option>') + if (data.projects) { + data.projects.sort(function (a, b) { + return (a.path_with_namespace < b.path_with_namespace) ? -1 : ((a.path_with_namespace > b.path_with_namespace) ? 1 : 0) + }) + data.projects.forEach(function (project) { + if (!project.snippets_enabled || + (project.permissions.project_access === null && project.permissions.group_access === null) || + (project.permissions.project_access !== null && project.permissions.project_access.access_level < 20)) { + return + } + $('<option>').val(project.id).text(project.path_with_namespace).appendTo('#snippetImportModalProjects') + }) + $('#snippetImportModalProjects').prop('disabled', false) + } + $('#snippetImportModalLoading').hide() }) .fail(function (data) { - showMessageModal('<i class="fa fa-gitlab"></i> Import from Snippet', 'Unable to fetch gitlab parameters :(', '', '', false); + showMessageModal('<i class="fa fa-gitlab"></i> Import from Snippet', 'Unable to fetch gitlab parameters :(', '', '', false) }) .always(function () { - ui.spinner.hide(); - }); -}); -//import from clipboard + ui.spinner.hide() + }) +}) +// import from clipboard ui.toolbar.import.clipboard.click(function () { - //na -}); -//upload image + // na +}) +// upload image ui.toolbar.uploadImage.bind('change', function (e) { - var files = e.target.files || e.dataTransfer.files; - e.dataTransfer = {}; - e.dataTransfer.files = files; - inlineAttach.onDrop(e); -}); -//toc + var files = e.target.files || e.dataTransfer.files + e.dataTransfer = {} + e.dataTransfer.files = files + inlineAttach.onDrop(e) +}) +// toc ui.toc.dropdown.click(function (e) { - e.stopPropagation(); -}); + e.stopPropagation() +}) // prevent empty link change hash $('a[href="#"]').click(function (e) { - e.preventDefault(); -}); - -//modal actions -var revisions = []; -var revisionViewer = null; -var revisionInsert = []; -var revisionDelete = []; -var revisionInsertAnnotation = null; -var revisionDeleteAnnotation = null; -var revisionList = ui.modal.revision.find('.ui-revision-list'); -var revision = null; -var revisionTime = null; + e.preventDefault() +}) + +// modal actions +var revisions = [] +var revisionViewer = null +var revisionInsert = [] +var revisionDelete = [] +var revisionInsertAnnotation = null +var revisionDeleteAnnotation = null +var revisionList = ui.modal.revision.find('.ui-revision-list') +var revision = null +var revisionTime = null ui.modal.revision.on('show.bs.modal', function (e) { - $.get(noteurl + '/revision') - .done(function(data) { - parseRevisions(data.revision); - initRevisionViewer(); + $.get(noteurl + '/revision') + .done(function (data) { + parseRevisions(data.revision) + initRevisionViewer() }) - .fail(function(err) { - + .fail(function (err) { + if (debug) { + console.log(err) + } }) - .always(function() { - //na - }); -}); -function checkRevisionViewer() { - if (revisionViewer) { - var container = $(revisionViewer.display.wrapper).parent(); - $(revisionViewer.display.scroller).css('height', container.height() + 'px'); - revisionViewer.refresh(); - } -} -ui.modal.revision.on('shown.bs.modal', checkRevisionViewer); -$(window).resize(checkRevisionViewer); -function parseRevisions(_revisions) { - if (_revisions.length != revisions) { - revisions = _revisions; - var lastRevision = null; - if (revisionList.children().length > 0) { - lastRevision = revisionList.find('.active').attr('data-revision-time'); - } - revisionList.html(''); - for (var i = 0; i < revisions.length; i++) { - var revision = revisions[i]; - var item = $('<a href="#" class="list-group-item"></a>'); - item.attr('data-revision-time', revision.time); - if (lastRevision == revision.time) item.addClass('active'); - var itemHeading = $('<h5 class="list-group-item-heading"></h5>'); - itemHeading.html('<i class="fa fa-clock-o"></i> ' + moment(revision.time).format('llll')); - var itemText = $('<p class="list-group-item-text"></p>'); - itemText.html('<i class="fa fa-file-text"></i> Length: ' + revision.length); - item.append(itemHeading).append(itemText); - item.click(function (e) { - var time = $(this).attr('data-revision-time'); - selectRevision(time); - }); - revisionList.append(item); - } - if (!lastRevision) { - selectRevision(revisions[0].time); - } - } -} -function selectRevision(time) { - if (time == revisionTime) return; - $.get(noteurl + '/revision/' + time) - .done(function(data) { - revision = data; - revisionTime = time; - var lastScrollInfo = revisionViewer.getScrollInfo(); - revisionList.children().removeClass('active'); - revisionList.find('[data-revision-time="' + time + '"]').addClass('active'); - var content = revision.content; - revisionViewer.setValue(content); - revisionViewer.scrollTo(null, lastScrollInfo.top); - revisionInsert = []; - revisionDelete = []; + .always(function () { + // na + }) +}) +function checkRevisionViewer () { + if (revisionViewer) { + var container = $(revisionViewer.display.wrapper).parent() + $(revisionViewer.display.scroller).css('height', container.height() + 'px') + revisionViewer.refresh() + } +} +ui.modal.revision.on('shown.bs.modal', checkRevisionViewer) +$(window).resize(checkRevisionViewer) +function parseRevisions (_revisions) { + if (_revisions.length !== revisions) { + revisions = _revisions + var lastRevision = null + if (revisionList.children().length > 0) { + lastRevision = revisionList.find('.active').attr('data-revision-time') + } + revisionList.html('') + for (var i = 0; i < revisions.length; i++) { + var revision = revisions[i] + var item = $('<a href="#" class="list-group-item"></a>') + item.attr('data-revision-time', revision.time) + if (lastRevision === revision.time) item.addClass('active') + var itemHeading = $('<h5 class="list-group-item-heading"></h5>') + itemHeading.html('<i class="fa fa-clock-o"></i> ' + moment(revision.time).format('llll')) + var itemText = $('<p class="list-group-item-text"></p>') + itemText.html('<i class="fa fa-file-text"></i> Length: ' + revision.length) + item.append(itemHeading).append(itemText) + item.click(function (e) { + var time = $(this).attr('data-revision-time') + selectRevision(time) + }) + revisionList.append(item) + } + if (!lastRevision) { + selectRevision(revisions[0].time) + } + } +} +function selectRevision (time) { + if (time === revisionTime) return + $.get(noteurl + '/revision/' + time) + .done(function (data) { + revision = data + revisionTime = time + var lastScrollInfo = revisionViewer.getScrollInfo() + revisionList.children().removeClass('active') + revisionList.find('[data-revision-time="' + time + '"]').addClass('active') + var content = revision.content + revisionViewer.setValue(content) + revisionViewer.scrollTo(null, lastScrollInfo.top) + revisionInsert = [] + revisionDelete = [] // mark the text which have been insert or delete - if (revision.patch.length > 0) { - var bias = 0; - for (var j = 0; j < revision.patch.length; j++) { - var patch = revision.patch[j]; - var currIndex = patch.start1 + bias; - for (var i = 0; i < patch.diffs.length; i++) { - var diff = patch.diffs[i]; + if (revision.patch.length > 0) { + var bias = 0 + for (var j = 0; j < revision.patch.length; j++) { + var patch = revision.patch[j] + var currIndex = patch.start1 + bias + for (var i = 0; i < patch.diffs.length; i++) { + var diff = patch.diffs[i] // ignore if diff only contains line breaks - if ((diff[1].match(new RegExp("\n", "g")) || []).length == diff[1].length) continue; - switch(diff[0]) { - case 0: // retain - currIndex += diff[1].length; - break; - case 1: // insert - var prePos = revisionViewer.posFromIndex(currIndex); - var postPos = revisionViewer.posFromIndex(currIndex + diff[1].length); - revisionInsert.push({ - from: prePos, - to: postPos - }); - revisionViewer.markText(prePos, postPos, { - css: 'background-color: rgba(230,255,230,0.7); text-decoration: underline;' - }); - currIndex += diff[1].length; - break; - case -1: // delete - var prePos = revisionViewer.posFromIndex(currIndex); - revisionViewer.replaceRange(diff[1], prePos); - var postPos = revisionViewer.posFromIndex(currIndex + diff[1].length); - revisionDelete.push({ - from: prePos, - to: postPos - }); - revisionViewer.markText(prePos, postPos, { - css: 'background-color: rgba(255,230,230,0.7); text-decoration: line-through;' - }); - bias += diff[1].length; - currIndex += diff[1].length; - break; - } - } + if ((diff[1].match(/\n/g) || []).length === diff[1].length) continue + var prePos + var postPos + switch (diff[0]) { + case 0: // retain + currIndex += diff[1].length + break + case 1: // insert + prePos = revisionViewer.posFromIndex(currIndex) + postPos = revisionViewer.posFromIndex(currIndex + diff[1].length) + revisionInsert.push({ + from: prePos, + to: postPos + }) + revisionViewer.markText(prePos, postPos, { + css: 'background-color: rgba(230,255,230,0.7); text-decoration: underline;' + }) + currIndex += diff[1].length + break + case -1: // delete + prePos = revisionViewer.posFromIndex(currIndex) + revisionViewer.replaceRange(diff[1], prePos) + postPos = revisionViewer.posFromIndex(currIndex + diff[1].length) + revisionDelete.push({ + from: prePos, + to: postPos + }) + revisionViewer.markText(prePos, postPos, { + css: 'background-color: rgba(255,230,230,0.7); text-decoration: line-through;' + }) + bias += diff[1].length + currIndex += diff[1].length + break } + } } - revisionInsertAnnotation.update(revisionInsert); - revisionDeleteAnnotation.update(revisionDelete); + } + revisionInsertAnnotation.update(revisionInsert) + revisionDeleteAnnotation.update(revisionDelete) }) - .fail(function(err) { - + .fail(function (err) { + if (debug) { + console.log(err) + } + }) + .always(function () { + // na }) - .always(function() { - //na - }); -} -function initRevisionViewer() { - if (revisionViewer) return; - var revisionViewerTextArea = document.getElementById("revisionViewer"); - revisionViewer = CodeMirror.fromTextArea(revisionViewerTextArea, { - mode: defaultEditorMode, - viewportMargin: viewportMargin, - lineNumbers: true, - lineWrapping: true, - showCursorWhenSelecting: true, - inputStyle: "textarea", - gutters: ["CodeMirror-linenumbers"], - flattenSpans: true, - addModeClass: true, - readOnly: true, - autoRefresh: true, - scrollbarStyle: 'overlay' - }); - revisionInsertAnnotation = revisionViewer.annotateScrollbar({ className:"CodeMirror-insert-match" }); - revisionDeleteAnnotation = revisionViewer.annotateScrollbar({ className:"CodeMirror-delete-match" }); - checkRevisionViewer(); +} +function initRevisionViewer () { + if (revisionViewer) return + var revisionViewerTextArea = document.getElementById('revisionViewer') + revisionViewer = CodeMirror.fromTextArea(revisionViewerTextArea, { + mode: defaultEditorMode, + viewportMargin: viewportMargin, + lineNumbers: true, + lineWrapping: true, + showCursorWhenSelecting: true, + inputStyle: 'textarea', + gutters: ['CodeMirror-linenumbers'], + flattenSpans: true, + addModeClass: true, + readOnly: true, + autoRefresh: true, + scrollbarStyle: 'overlay' + }) + revisionInsertAnnotation = revisionViewer.annotateScrollbar({ className: 'CodeMirror-insert-match' }) + revisionDeleteAnnotation = revisionViewer.annotateScrollbar({ className: 'CodeMirror-delete-match' }) + checkRevisionViewer() } $('#revisionModalDownload').click(function () { - if (!revision) return; - var filename = renderFilename(ui.area.markdown) + '_' + revisionTime + '.md'; - var blob = new Blob([revision.content], { - type: "text/markdown;charset=utf-8" - }); - saveAs(blob, filename, true); -}); + if (!revision) return + var filename = renderFilename(ui.area.markdown) + '_' + revisionTime + '.md' + var blob = new Blob([revision.content], { + type: 'text/markdown;charset=utf-8' + }) + saveAs(blob, filename, true) +}) $('#revisionModalRevert').click(function () { - if (!revision) return; - editor.setValue(revision.content); - ui.modal.revision.modal('hide'); -}); -//snippet projects -ui.modal.snippetImportProjects.change(function() { - var accesstoken = $("#snippetImportModalAccessToken").val(), - baseURL = $("#snippetImportModalBaseURL").val(), - project = $("#snippetImportModalProjects").val(); - - $("#snippetImportModalLoading").show(); - $("#snippetImportModalContent").val('/projects/' + project); - $.get(baseURL + '/api/v3/projects/' + project + '/snippets?access_token=' + accesstoken) - .done(function(data) { - $("#snippetImportModalSnippets").find('option').remove().end().append('<option value="init" selected="selected" disabled="disabled">Select From Available Snippets</option>'); - data.forEach(function(snippet) { - $('<option>').val(snippet.id).text(snippet.title).appendTo($("#snippetImportModalSnippets")); - }); - $("#snippetImportModalLoading").hide(); - $("#snippetImportModalSnippets").prop('disabled', false); + if (!revision) return + editor.setValue(revision.content) + ui.modal.revision.modal('hide') +}) +// snippet projects +ui.modal.snippetImportProjects.change(function () { + var accesstoken = $('#snippetImportModalAccessToken').val() + var baseURL = $('#snippetImportModalBaseURL').val() + var project = $('#snippetImportModalProjects').val() + + $('#snippetImportModalLoading').show() + $('#snippetImportModalContent').val('/projects/' + project) + $.get(baseURL + '/api/v3/projects/' + project + '/snippets?access_token=' + accesstoken) + .done(function (data) { + $('#snippetImportModalSnippets').find('option').remove().end().append('<option value="init" selected="selected" disabled="disabled">Select From Available Snippets</option>') + data.forEach(function (snippet) { + $('<option>').val(snippet.id).text(snippet.title).appendTo($('#snippetImportModalSnippets')) + }) + $('#snippetImportModalLoading').hide() + $('#snippetImportModalSnippets').prop('disabled', false) }) - .fail(function(err) { - + .fail(function (err) { + if (debug) { + console.log(err) + } }) - .always(function() { - //na - }); -}); -//snippet snippets -ui.modal.snippetImportSnippets.change(function() { - var project = $("#snippetImportModalProjects").val(), - snippet = $("#snippetImportModalSnippets").val(); - - $("#snippetImportModalContent").val($("#snippetImportModalContent").val() + '/snippets/' + snippet); -}) - -function scrollToTop() { - if (currentMode == modeType.both) { - if (editor.getScrollInfo().top != 0) - editor.scrollTo(0, 0); - else - ui.area.view.animate({ - scrollTop: 0 - }, 100, "linear"); - } else { - $('body, html').stop(true, true).animate({ - scrollTop: 0 - }, 100, "linear"); - } -} - -function scrollToBottom() { - if (currentMode == modeType.both) { - var scrollInfo = editor.getScrollInfo(); - var scrollHeight = scrollInfo.height; - if (scrollInfo.top != scrollHeight) - editor.scrollTo(0, scrollHeight * 2); - else - ui.area.view.animate({ - scrollTop: ui.area.view[0].scrollHeight - }, 100, "linear"); - } else { - $('body, html').stop(true, true).animate({ - scrollTop: $(document.body)[0].scrollHeight - }, 100, "linear"); - } -} - -window.scrollToTop = scrollToTop; -window.scrollToBottom = scrollToBottom; - -var enoughForAffixToc = true; - -//scrollspy -function generateScrollspy() { - $(document.body).scrollspy({ - target: '.scrollspy-body' - }); - ui.area.view.scrollspy({ - target: '.scrollspy-view' - }); - $(document.body).scrollspy('refresh'); - ui.area.view.scrollspy('refresh'); - if (enoughForAffixToc) { - ui.toc.toc.hide(); - ui.toc.affix.show(); - } else { - ui.toc.affix.hide(); - ui.toc.toc.show(); - } - //$(document.body).scroll(); - //ui.area.view.scroll(); -} + .always(function () { + // na + }) +}) +// snippet snippets +ui.modal.snippetImportSnippets.change(function () { + var snippet = $('#snippetImportModalSnippets').val() + $('#snippetImportModalContent').val($('#snippetImportModalContent').val() + '/snippets/' + snippet) +}) -function updateScrollspy() { - var headers = ui.area.markdown.find('h1, h2, h3').toArray(); - var headerMap = []; - for (var i = 0; i < headers.length; i++) { - headerMap.push($(headers[i]).offset().top - parseInt($(headers[i]).css('margin-top'))); - } - applyScrollspyActive($(window).scrollTop(), headerMap, headers, - $('.scrollspy-body'), 0); - var offset = ui.area.view.scrollTop() - ui.area.view.offset().top; - applyScrollspyActive(ui.area.view.scrollTop(), headerMap, headers, - $('.scrollspy-view'), offset - 10); -} - -function applyScrollspyActive(top, headerMap, headers, target, offset) { - var index = 0; - for (var i = headerMap.length - 1; i >= 0; i--) { - if (top >= (headerMap[i] + offset) && headerMap[i + 1] && top < (headerMap[i + 1] + offset)) { - index = i; - break; - } - } - var header = $(headers[index]); - var active = target.find('a[href="#' + header.attr('id') + '"]'); - active.closest('li').addClass('active').parent().closest('li').addClass('active').parent().closest('li').addClass('active'); +function scrollToTop () { + if (window.currentMode === modeType.both) { + if (editor.getScrollInfo().top !== 0) { editor.scrollTo(0, 0) } else { + ui.area.view.animate({ + scrollTop: 0 + }, 100, 'linear') + } + } else { + $('body, html').stop(true, true).animate({ + scrollTop: 0 + }, 100, 'linear') + } +} + +function scrollToBottom () { + if (window.currentMode === modeType.both) { + var scrollInfo = editor.getScrollInfo() + var scrollHeight = scrollInfo.height + if (scrollInfo.top !== scrollHeight) { editor.scrollTo(0, scrollHeight * 2) } else { + ui.area.view.animate({ + scrollTop: ui.area.view[0].scrollHeight + }, 100, 'linear') + } + } else { + $('body, html').stop(true, true).animate({ + scrollTop: $(document.body)[0].scrollHeight + }, 100, 'linear') + } +} + +window.scrollToTop = scrollToTop +window.scrollToBottom = scrollToBottom + +var enoughForAffixToc = true + +// scrollspy +function generateScrollspy () { + $(document.body).scrollspy({ + target: '.scrollspy-body' + }) + ui.area.view.scrollspy({ + target: '.scrollspy-view' + }) + $(document.body).scrollspy('refresh') + ui.area.view.scrollspy('refresh') + if (enoughForAffixToc) { + ui.toc.toc.hide() + ui.toc.affix.show() + } else { + ui.toc.affix.hide() + ui.toc.toc.show() + } + // $(document.body).scroll(); + // ui.area.view.scroll(); +} + +function updateScrollspy () { + var headers = ui.area.markdown.find('h1, h2, h3').toArray() + var headerMap = [] + for (var i = 0; i < headers.length; i++) { + headerMap.push($(headers[i]).offset().top - parseInt($(headers[i]).css('margin-top'))) + } + applyScrollspyActive($(window).scrollTop(), headerMap, headers, + $('.scrollspy-body'), 0) + var offset = ui.area.view.scrollTop() - ui.area.view.offset().top + applyScrollspyActive(ui.area.view.scrollTop(), headerMap, headers, + $('.scrollspy-view'), offset - 10) +} + +function applyScrollspyActive (top, headerMap, headers, target, offset) { + var index = 0 + for (var i = headerMap.length - 1; i >= 0; i--) { + if (top >= (headerMap[i] + offset) && headerMap[i + 1] && top < (headerMap[i + 1] + offset)) { + index = i + break + } + } + var header = $(headers[index]) + var active = target.find('a[href="#' + header.attr('id') + '"]') + active.closest('li').addClass('active').parent().closest('li').addClass('active').parent().closest('li').addClass('active') } // clipboard modal -//fix for wrong autofocus +// fix for wrong autofocus $('#clipboardModal').on('shown.bs.modal', function () { - $('#clipboardModal').blur(); -}); -$("#clipboardModalClear").click(function () { - $("#clipboardModalContent").html(''); -}); -$("#clipboardModalConfirm").click(function () { - var data = $("#clipboardModalContent").html(); - if (data) { - parseToEditor(data); - $('#clipboardModal').modal('hide'); - $("#clipboardModalContent").html(''); - } -}); + $('#clipboardModal').blur() +}) +$('#clipboardModalClear').click(function () { + $('#clipboardModalContent').html('') +}) +$('#clipboardModalConfirm').click(function () { + var data = $('#clipboardModalContent').html() + if (data) { + parseToEditor(data) + $('#clipboardModal').modal('hide') + $('#clipboardModalContent').html('') + } +}) // refresh modal $('#refreshModalRefresh').click(function () { - location.reload(true); -}); + location.reload(true) +}) // gist import modal -$("#gistImportModalClear").click(function () { - $("#gistImportModalContent").val(''); -}); -$("#gistImportModalConfirm").click(function () { - var gisturl = $("#gistImportModalContent").val(); - if (!gisturl) return; - $('#gistImportModal').modal('hide'); - $("#gistImportModalContent").val(''); - if (!isValidURL(gisturl)) { - showMessageModal('<i class="fa fa-github"></i> Import from Gist', 'Not a valid URL :(', '', '', false); - return; +$('#gistImportModalClear').click(function () { + $('#gistImportModalContent').val('') +}) +$('#gistImportModalConfirm').click(function () { + var gisturl = $('#gistImportModalContent').val() + if (!gisturl) return + $('#gistImportModal').modal('hide') + $('#gistImportModalContent').val('') + if (!isValidURL(gisturl)) { + showMessageModal('<i class="fa fa-github"></i> Import from Gist', 'Not a valid URL :(', '', '', false) + } else { + var hostname = window.url('hostname', gisturl) + if (hostname !== 'gist.github.com') { + showMessageModal('<i class="fa fa-github"></i> Import from Gist', 'Not a valid Gist URL :(', '', '', false) } else { - var hostname = url('hostname', gisturl) - if (hostname !== 'gist.github.com') { - showMessageModal('<i class="fa fa-github"></i> Import from Gist', 'Not a valid Gist URL :(', '', '', false); - } else { - ui.spinner.show(); - $.get('https://api.github.com/gists/' + url('-1', gisturl)) + ui.spinner.show() + $.get('https://api.github.com/gists/' + window.url('-1', gisturl)) .done(function (data) { - if (data.files) { - var contents = ""; - Object.keys(data.files).forEach(function (key) { - contents += key; - contents += '\n---\n'; - contents += data.files[key].content; - contents += '\n\n'; - }); - replaceAll(contents); - } else { - showMessageModal('<i class="fa fa-github"></i> Import from Gist', 'Unable to fetch gist files :(', '', '', false); - } + if (data.files) { + var contents = '' + Object.keys(data.files).forEach(function (key) { + contents += key + contents += '\n---\n' + contents += data.files[key].content + contents += '\n\n' + }) + replaceAll(contents) + } else { + showMessageModal('<i class="fa fa-github"></i> Import from Gist', 'Unable to fetch gist files :(', '', '', false) + } }) .fail(function (data) { - showMessageModal('<i class="fa fa-github"></i> Import from Gist', 'Not a valid Gist URL :(', '', JSON.stringify(data), false); + showMessageModal('<i class="fa fa-github"></i> Import from Gist', 'Not a valid Gist URL :(', '', JSON.stringify(data), false) }) .always(function () { - ui.spinner.hide(); - }); - } + ui.spinner.hide() + }) } -}); + } +}) // snippet import modal -$("#snippetImportModalClear").click(function () { - $("#snippetImportModalContent").val(''); - $("#snippetImportModalProjects").val('init'); - $("#snippetImportModalSnippets").val('init'); - $("#snippetImportModalSnippets").prop('disabled', true); -}); -$("#snippetImportModalConfirm").click(function () { - var snippeturl = $("#snippetImportModalContent").val(); - if (!snippeturl) return; - $('#snippetImportModal').modal('hide'); - $("#snippetImportModalContent").val(''); - if (!/^.+\/snippets\/.+$/.test(snippeturl)) { - showMessageModal('<i class="fa fa-github"></i> Import from Snippet', 'Not a valid Snippet URL :(', '', '', false); - } else { - ui.spinner.show(); - var accessToken = '?access_token=' + $("#snippetImportModalAccessToken").val(); - var fullURL = $("#snippetImportModalBaseURL").val() + '/api/v3' + snippeturl; - $.get(fullURL + accessToken) - .done(function(data) { - var content = '# ' + (data.title || "Snippet Import"); - var fileInfo = data.file_name.split('.'); - fileInfo[1] = (fileInfo[1]) ? fileInfo[1] : "md"; - $.get(fullURL + '/raw' + accessToken) +$('#snippetImportModalClear').click(function () { + $('#snippetImportModalContent').val('') + $('#snippetImportModalProjects').val('init') + $('#snippetImportModalSnippets').val('init') + $('#snippetImportModalSnippets').prop('disabled', true) +}) +$('#snippetImportModalConfirm').click(function () { + var snippeturl = $('#snippetImportModalContent').val() + if (!snippeturl) return + $('#snippetImportModal').modal('hide') + $('#snippetImportModalContent').val('') + if (!/^.+\/snippets\/.+$/.test(snippeturl)) { + showMessageModal('<i class="fa fa-github"></i> Import from Snippet', 'Not a valid Snippet URL :(', '', '', false) + } else { + ui.spinner.show() + var accessToken = '?access_token=' + $('#snippetImportModalAccessToken').val() + var fullURL = $('#snippetImportModalBaseURL').val() + '/api/v3' + snippeturl + $.get(fullURL + accessToken) + .done(function (data) { + var content = '# ' + (data.title || 'Snippet Import') + var fileInfo = data.file_name.split('.') + fileInfo[1] = (fileInfo[1]) ? fileInfo[1] : 'md' + $.get(fullURL + '/raw' + accessToken) .done(function (raw) { - if (raw) { - content += "\n\n"; - if (fileInfo[1] != "md") { - content += "```" + fileTypes[fileInfo[1]] + "\n"; - } - content += raw; - if (fileInfo[1] != "md") { - content += "\n```"; - } - replaceAll(content); + if (raw) { + content += '\n\n' + if (fileInfo[1] !== 'md') { + content += '```' + window.fileTypes[fileInfo[1]] + '\n' + } + content += raw + if (fileInfo[1] !== 'md') { + content += '\n```' } + replaceAll(content) + } }) .fail(function (data) { - showMessageModal('<i class="fa fa-gitlab"></i> Import from Snippet', 'Not a valid Snippet URL :(', '', JSON.stringify(data), false); + showMessageModal('<i class="fa fa-gitlab"></i> Import from Snippet', 'Not a valid Snippet URL :(', '', JSON.stringify(data), false) }) .always(function () { - ui.spinner.hide(); - }); + ui.spinner.hide() + }) }) .fail(function (data) { - showMessageModal('<i class="fa fa-gitlab"></i> Import from Snippet', 'Not a valid Snippet URL :(', '', JSON.stringify(data), false); - }); - } -}); - -//snippet export modal -$("#snippetExportModalConfirm").click(function() { - var accesstoken = $("#snippetExportModalAccessToken").val(), - baseURL = $("#snippetExportModalBaseURL").val(), - data = { - title: $("#snippetExportModalTitle").val(), - file_name: $("#snippetExportModalFileName").val(), - code: editor.getValue(), - visibility_level: $("#snippetExportModalVisibility").val() - }; - if (!data.title || !data.file_name || !data.code || !data.visibility_level || !$("#snippetExportModalProjects").val()) return; - $("#snippetExportModalLoading").show(); - var fullURL = baseURL + '/api/v3/projects/' + $("#snippetExportModalProjects").val() + '/snippets?access_token=' + accesstoken; - $.post(fullURL + showMessageModal('<i class="fa fa-gitlab"></i> Import from Snippet', 'Not a valid Snippet URL :(', '', JSON.stringify(data), false) + }) + } +}) + +// snippet export modal +$('#snippetExportModalConfirm').click(function () { + var accesstoken = $('#snippetExportModalAccessToken').val() + var baseURL = $('#snippetExportModalBaseURL').val() + var data = { + title: $('#snippetExportModalTitle').val(), + file_name: $('#snippetExportModalFileName').val(), + code: editor.getValue(), + visibility_level: $('#snippetExportModalVisibility').val() + } + if (!data.title || !data.file_name || !data.code || !data.visibility_level || !$('#snippetExportModalProjects').val()) return + $('#snippetExportModalLoading').show() + var fullURL = baseURL + '/api/v3/projects/' + $('#snippetExportModalProjects').val() + '/snippets?access_token=' + accesstoken + $.post(fullURL , data - , function(ret) { - $("#snippetExportModalLoading").hide(); - $('#snippetExportModal').modal('hide'); - var redirect = baseURL + '/' + $("#snippetExportModalProjects option[value='" + $("#snippetExportModalProjects").val() + "']").text() + '/snippets/' + ret.id; - showMessageModal('<i class="fa fa-gitlab"></i> Export to Snippet', 'Export Successful!', redirect, 'View Snippet Here', true); + , function (ret) { + $('#snippetExportModalLoading').hide() + $('#snippetExportModal').modal('hide') + var redirect = baseURL + '/' + $("#snippetExportModalProjects option[value='" + $('#snippetExportModalProjects').val() + "']").text() + '/snippets/' + ret.id + showMessageModal('<i class="fa fa-gitlab"></i> Export to Snippet', 'Export Successful!', redirect, 'View Snippet Here', true) } , 'json' - ); -}); - -function parseToEditor(data) { - var parsed = toMarkdown(data); - if (parsed) - replaceAll(parsed); -} - -function replaceAll(data) { - editor.replaceRange(data, { - line: 0, - ch: 0 - }, { - line: editor.lastLine(), - ch: editor.lastLine().length - }, '+input'); -} - -function importFromUrl(url) { - //console.log(url); - if (!url) return; - if (!isValidURL(url)) { - showMessageModal('<i class="fa fa-cloud-download"></i> Import from URL', 'Not a valid URL :(', '', '', false); - return; - } - $.ajax({ - method: "GET", - url: url, - success: function (data) { - var extension = url.split('.').pop(); - if (extension == 'html') - parseToEditor(data); - else - replaceAll(data); - }, - error: function (data) { - showMessageModal('<i class="fa fa-cloud-download"></i> Import from URL', 'Import failed :(', '', JSON.stringify(data), false); - }, - complete: function () { - ui.spinner.hide(); - } - }); + ) +}) + +function parseToEditor (data) { + var parsed = toMarkdown(data) + if (parsed) { replaceAll(parsed) } +} + +function replaceAll (data) { + editor.replaceRange(data, { + line: 0, + ch: 0 + }, { + line: editor.lastLine(), + ch: editor.lastLine().length + }, '+input') +} + +function importFromUrl (url) { + // console.log(url); + if (!url) return + if (!isValidURL(url)) { + showMessageModal('<i class="fa fa-cloud-download"></i> Import from URL', 'Not a valid URL :(', '', '', false) + return + } + $.ajax({ + method: 'GET', + url: url, + success: function (data) { + var extension = url.split('.').pop() + if (extension === 'html') { parseToEditor(data) } else { replaceAll(data) } + }, + error: function (data) { + showMessageModal('<i class="fa fa-cloud-download"></i> Import from URL', 'Import failed :(', '', JSON.stringify(data), false) + }, + complete: function () { + ui.spinner.hide() + } + }) } -//mode +// mode ui.toolbar.mode.click(function () { - toggleMode(); -}); -//edit + toggleMode() +}) +// edit ui.toolbar.edit.click(function () { - changeMode(modeType.edit); -}); -//view + changeMode(modeType.edit) +}) +// view ui.toolbar.view.click(function () { - changeMode(modeType.view); -}); -//both + changeMode(modeType.view) +}) +// both ui.toolbar.both.click(function () { - changeMode(modeType.both); -}); -//permission -//freely + changeMode(modeType.both) +}) +// permission +// freely ui.infobar.permission.freely.click(function () { - emitPermission("freely"); -}); -//editable + emitPermission('freely') +}) +// editable ui.infobar.permission.editable.click(function () { - emitPermission("editable"); -}); -//locked + emitPermission('editable') +}) +// locked ui.infobar.permission.locked.click(function () { - emitPermission("locked"); -}); -//private + emitPermission('locked') +}) +// private ui.infobar.permission.private.click(function () { - emitPermission("private"); -}); -//limited -ui.infobar.permission.limited.click(function() { - emitPermission("limited"); -}); -//protected -ui.infobar.permission.protected.click(function() { - emitPermission("protected"); -}); + emitPermission('private') +}) +// limited +ui.infobar.permission.limited.click(function () { + emitPermission('limited') +}) +// protected +ui.infobar.permission.protected.click(function () { + emitPermission('protected') +}) // delete note ui.infobar.delete.click(function () { - $('.delete-modal').modal('show'); -}); + $('.delete-modal').modal('show') +}) $('.ui-delete-modal-confirm').click(function () { - socket.emit('delete'); -}); - -function emitPermission(_permission) { - if (_permission != permission) { - socket.emit('permission', _permission); - } -} - -function updatePermission(newPermission) { - if (permission != newPermission) { - permission = newPermission; - if (loaded) refreshView(); - } - var label = null; - var title = null; - switch (permission) { - case "freely": - label = '<i class="fa fa-leaf"></i> Freely'; - title = "Anyone can edit"; - break; - case "editable": - label = '<i class="fa fa-shield"></i> Editable'; - title = "Signed people can edit"; - break; - case "limited": - label = '<i class="fa fa-id-card"></i> Limited'; - title = "Signed people can edit (forbid guest)" - break; - case "locked": - label = '<i class="fa fa-lock"></i> Locked'; - title = "Only owner can edit"; - break; - case "protected": - label = '<i class="fa fa-umbrella"></i> Protected'; - title = "Only owner can edit (forbid guest)"; - break; - case "private": - label = '<i class="fa fa-hand-stop-o"></i> Private'; - title = "Only owner can view & edit"; - break; - } - if (personalInfo.userid && owner && personalInfo.userid == owner) { - label += ' <i class="fa fa-caret-down"></i>'; - ui.infobar.permission.label.removeClass('disabled'); - } else { - ui.infobar.permission.label.addClass('disabled'); - } - ui.infobar.permission.label.html(label).attr('title', title); -} - -function havePermission() { - var bool = false; - switch (permission) { - case "freely": - bool = true; - break; - case "editable": - case "limited": - if (!personalInfo.login) { - bool = false; - } else { - bool = true; - } - break; - case "locked": - case "private": - case "protected": - if (!owner || personalInfo.userid != owner) { - bool = false; - } else { - bool = true; - } - break; - } - return bool; + socket.emit('delete') +}) + +function emitPermission (_permission) { + if (_permission !== permission) { + socket.emit('permission', _permission) + } +} + +function updatePermission (newPermission) { + if (permission !== newPermission) { + permission = newPermission + if (window.loaded) refreshView() + } + var label = null + var title = null + switch (permission) { + case 'freely': + label = '<i class="fa fa-leaf"></i> Freely' + title = 'Anyone can edit' + break + case 'editable': + label = '<i class="fa fa-shield"></i> Editable' + title = 'Signed people can edit' + break + case 'limited': + label = '<i class="fa fa-id-card"></i> Limited' + title = 'Signed people can edit (forbid guest)' + break + case 'locked': + label = '<i class="fa fa-lock"></i> Locked' + title = 'Only owner can edit' + break + case 'protected': + label = '<i class="fa fa-umbrella"></i> Protected' + title = 'Only owner can edit (forbid guest)' + break + case 'private': + label = '<i class="fa fa-hand-stop-o"></i> Private' + title = 'Only owner can view & edit' + break + } + if (window.personalInfo.userid && window.owner && window.personalInfo.userid === window.owner) { + label += ' <i class="fa fa-caret-down"></i>' + ui.infobar.permission.label.removeClass('disabled') + } else { + ui.infobar.permission.label.addClass('disabled') + } + ui.infobar.permission.label.html(label).attr('title', title) +} + +function havePermission () { + var bool = false + switch (permission) { + case 'freely': + bool = true + break + case 'editable': + case 'limited': + if (!window.personalInfo.login) { + bool = false + } else { + bool = true + } + break + case 'locked': + case 'private': + case 'protected': + if (!window.owner || window.personalInfo.userid !== window.owner) { + bool = false + } else { + bool = true + } + break + } + return bool } // global module workaround -window.havePermission = havePermission; +window.havePermission = havePermission -//socket.io actions -var io = require("socket.io-client"); +// socket.io actions +var io = require('socket.io-client') var socket = io.connect({ - path: urlpath ? '/' + urlpath + '/socket.io/' : '', - timeout: 5000, //5 secs to timeout, - reconnectionAttempts: 20 // retry 20 times on connect failed -}); -//overwrite original event for checking login state -var on = socket.on; + path: urlpath ? '/' + urlpath + '/socket.io/' : '', + timeout: 5000, // 5 secs to timeout, + reconnectionAttempts: 20 // retry 20 times on connect failed +}) +// overwrite original event for checking login state +var on = socket.on socket.on = function () { - if (!checkLoginStateChanged() && !needRefresh) - return on.apply(socket, arguments); -}; -var emit = socket.emit; + if (!checkLoginStateChanged() && !window.needRefresh) { return on.apply(socket, arguments) } +} +var emit = socket.emit socket.emit = function () { - if (!checkLoginStateChanged() && !needRefresh) - emit.apply(socket, arguments); -}; + if (!checkLoginStateChanged() && !window.needRefresh) { emit.apply(socket, arguments) } +} socket.on('info', function (data) { - console.error(data); - switch (data.code) { - case 403: - location.href = serverurl + "/403"; - break; - case 404: - location.href = serverurl + "/404"; - break; - case 500: - location.href = serverurl + "/500"; - break; - } -}); + console.error(data) + switch (data.code) { + case 403: + location.href = serverurl + '/403' + break + case 404: + location.href = serverurl + '/404' + break + case 500: + location.href = serverurl + '/500' + break + } +}) socket.on('error', function (data) { - console.error(data); - if (data.message && data.message.indexOf('AUTH failed') === 0) - location.href = serverurl + "/403"; -}); + console.error(data) + if (data.message && data.message.indexOf('AUTH failed') === 0) { location.href = serverurl + '/403' } +}) socket.on('delete', function () { - if (personalInfo.login) { - deleteServerHistory(noteid, function (err, data) { - if (!err) location.href = serverurl; - }); - } else { - getHistory(function (notehistory) { - var newnotehistory = removeHistory(noteid, notehistory); - saveHistory(newnotehistory); - location.href = serverurl; - }); - } -}); -var retryTimer = null; + if (window.personalInfo.login) { + deleteServerHistory(noteid, function (err, data) { + if (!err) location.href = serverurl + }) + } else { + getHistory(function (notehistory) { + var newnotehistory = removeHistory(noteid, notehistory) + saveHistory(newnotehistory) + location.href = serverurl + }) + } +}) +var retryTimer = null socket.on('maintenance', function () { - cmClient.revision = -1; -}); + cmClient.revision = -1 +}) socket.on('disconnect', function (data) { - showStatus(statusType.offline); - if (loaded) { - saveInfo(); - lastInfo.history = editor.getHistory(); - } - if (!editor.getOption('readOnly')) - editor.setOption('readOnly', true); - if (!retryTimer) { - retryTimer = setInterval(function () { - if (!needRefresh) socket.connect(); - }, 1000); - } -}); + showStatus(statusType.offline) + if (window.loaded) { + saveInfo() + window.lastInfo.history = editor.getHistory() + } + if (!editor.getOption('readOnly')) { editor.setOption('readOnly', true) } + if (!retryTimer) { + retryTimer = setInterval(function () { + if (!window.needRefresh) socket.connect() + }, 1000) + } +}) socket.on('reconnect', function (data) { - //sync back any change in offline - emitUserStatus(true); - cursorActivity(); - socket.emit('online users'); -}); + // sync back any change in offline + emitUserStatus(true) + cursorActivity() + socket.emit('online users') +}) socket.on('connect', function (data) { - clearInterval(retryTimer); - retryTimer = null; - personalInfo['id'] = socket.id; - showStatus(statusType.connected); - socket.emit('version'); -}); + clearInterval(retryTimer) + retryTimer = null + window.personalInfo['id'] = socket.id + showStatus(statusType.connected) + socket.emit('version') +}) socket.on('version', function (data) { - if (version != data.version) { - if (version < data.minimumCompatibleVersion) { - setRefreshModal('incompatible-version'); - setNeedRefresh(); - } else { - setRefreshModal('new-version'); - } - } -}); -var authors = []; -var authorship = []; -var authorshipMarks = {}; -var authorMarks = {}; // temp variable -var addTextMarkers = []; // temp variable -function updateInfo(data) { - //console.log(data); - if (data.hasOwnProperty('createtime') && createtime !== data.createtime) { - createtime = data.createtime; - updateLastChange(); - } - if (data.hasOwnProperty('updatetime') && lastchangetime !== data.updatetime) { - lastchangetime = data.updatetime; - updateLastChange(); - } - if (data.hasOwnProperty('owner') && owner !== data.owner) { - owner = data.owner; - ownerprofile = data.ownerprofile; - updateOwner(); - } - if (data.hasOwnProperty('lastchangeuser') && lastchangeuser !== data.lastchangeuser) { - lastchangeuser = data.lastchangeuser; - lastchangeuserprofile = data.lastchangeuserprofile; - updateLastChangeUser(); - updateOwner(); - } - if (data.hasOwnProperty('authors') && authors !== data.authors) { - authors = data.authors; - } - if (data.hasOwnProperty('authorship') && authorship !== data.authorship) { - authorship = data.authorship; - updateAuthorship(); + if (version !== data.version) { + if (version < data.minimumCompatibleVersion) { + setRefreshModal('incompatible-version') + setNeedRefresh() + } else { + setRefreshModal('new-version') } + } +}) +var authors = [] +var authorship = [] +var authorMarks = {} // temp variable +var addTextMarkers = [] // temp variable +function updateInfo (data) { + // console.log(data); + if (data.hasOwnProperty('createtime') && window.createtime !== data.createtime) { + window.createtime = data.createtime + updateLastChange() + } + if (data.hasOwnProperty('updatetime') && window.lastchangetime !== data.updatetime) { + window.lastchangetime = data.updatetime + updateLastChange() + } + if (data.hasOwnProperty('owner') && window.owner !== data.owner) { + window.owner = data.owner + window.ownerprofile = data.ownerprofile + updateOwner() + } + if (data.hasOwnProperty('lastchangeuser') && window.lastchangeuser !== data.lastchangeuser) { + window.lastchangeuser = data.lastchangeuser + window.lastchangeuserprofile = data.lastchangeuserprofile + updateLastChangeUser() + updateOwner() + } + if (data.hasOwnProperty('authors') && authors !== data.authors) { + authors = data.authors + } + if (data.hasOwnProperty('authorship') && authorship !== data.authorship) { + authorship = data.authorship + updateAuthorship() + } } var updateAuthorship = _.debounce(function () { - editor.operation(updateAuthorshipInner); -}, 50); -function initMark() { - return { - gutter: { - userid: null, - timestamp: null - }, - textmarkers: [] - }; -} -function initMarkAndCheckGutter(mark, author, timestamp) { - if (!mark) mark = initMark(); - if (!mark.gutter.userid || mark.gutter.timestamp > timestamp) { - mark.gutter.userid = author.userid; - mark.gutter.timestamp = timestamp; - } - return mark; -} -var gutterStylePrefix = "border-left: 3px solid "; -var gutterStylePostfix = "; height: " + defaultTextHeight + "px; margin-left: 3px;"; -var textMarkderStylePrefix = "background-image: linear-gradient(to top, "; -var textMarkderStylePostfix = " 1px, transparent 1px);"; + editor.operation(updateAuthorshipInner) +}, 50) +function initMark () { + return { + gutter: { + userid: null, + timestamp: null + }, + textmarkers: [] + } +} +function initMarkAndCheckGutter (mark, author, timestamp) { + if (!mark) mark = initMark() + if (!mark.gutter.userid || mark.gutter.timestamp > timestamp) { + mark.gutter.userid = author.userid + mark.gutter.timestamp = timestamp + } + return mark +} +var gutterStylePrefix = 'border-left: 3px solid ' +var gutterStylePostfix = '; height: ' + defaultTextHeight + 'px; margin-left: 3px;' +var textMarkderStylePrefix = 'background-image: linear-gradient(to top, ' +var textMarkderStylePostfix = ' 1px, transparent 1px);' var addStyleRule = (function () { - var added = {}; - var styleElement = document.createElement('style'); - document.documentElement.getElementsByTagName('head')[0].appendChild(styleElement); - var styleSheet = styleElement.sheet; - - return function (css) { - if (added[css]) { - return; - } - added[css] = true; - styleSheet.insertRule(css, (styleSheet.cssRules || styleSheet.rules).length); - }; -}()); -function updateAuthorshipInner() { + var added = {} + var styleElement = document.createElement('style') + document.documentElement.getElementsByTagName('head')[0].appendChild(styleElement) + var styleSheet = styleElement.sheet + + return function (css) { + if (added[css]) { + return + } + added[css] = true + styleSheet.insertRule(css, (styleSheet.cssRules || styleSheet.rules).length) + } +}()) +function updateAuthorshipInner () { // ignore when ot not synced yet - if (havePendingOperation()) return; - authorMarks = {}; - for (var i = 0; i < authorship.length; i++) { - var atom = authorship[i]; - var author = authors[atom[0]]; - if (author) { - var prePos = editor.posFromIndex(atom[1]); - var preLine = editor.getLine(prePos.line); - var postPos = editor.posFromIndex(atom[2]); - var postLine = editor.getLine(postPos.line); - if (prePos.ch == 0 && postPos.ch == postLine.length) { - for (var j = prePos.line; j <= postPos.line; j++) { - if (editor.getLine(j)) { - authorMarks[j] = initMarkAndCheckGutter(authorMarks[j], author, atom[3]); - } - } - } else if (postPos.line - prePos.line >= 1) { - var startLine = prePos.line; - var endLine = postPos.line; - if (prePos.ch == preLine.length) { - startLine++; - } else if (prePos.ch != 0) { - var mark = initMarkAndCheckGutter(authorMarks[prePos.line], author, atom[3]); - var _postPos = { - line: prePos.line, - ch: preLine.length - }; - if (JSON.stringify(prePos) != JSON.stringify(_postPos)) { - mark.textmarkers.push({ - userid: author.userid, - pos: [prePos, _postPos] - }); - startLine++; - } - authorMarks[prePos.line] = mark; - } - if (postPos.ch == 0) { - endLine--; - } else if (postPos.ch != postLine.length) { - var mark = initMarkAndCheckGutter(authorMarks[postPos.line], author, atom[3]); - var _prePos = { - line: postPos.line, - ch: 0 - }; - if (JSON.stringify(_prePos) != JSON.stringify(postPos)) { - mark.textmarkers.push({ - userid: author.userid, - pos: [_prePos, postPos] - }); - endLine--; - } - authorMarks[postPos.line] = mark; - } - for (var j = startLine; j <= endLine; j++) { - if (editor.getLine(j)) { - authorMarks[j] = initMarkAndCheckGutter(authorMarks[j], author, atom[3]); - } - } - } else { - var mark = initMarkAndCheckGutter(authorMarks[prePos.line], author, atom[3]); - if (JSON.stringify(prePos) != JSON.stringify(postPos)) { - mark.textmarkers.push({ - userid: author.userid, - pos: [prePos, postPos] - }); - } - authorMarks[prePos.line] = mark; - } - } - } - addTextMarkers = []; - editor.eachLine(iterateLine); - var allTextMarks = editor.getAllMarks(); - for (var i = 0; i < allTextMarks.length; i++) { - var _textMarker = allTextMarks[i]; - var pos = _textMarker.find(); - var found = false; - for (var j = 0; j < addTextMarkers.length; j++) { - var textMarker = addTextMarkers[j]; - var author = authors[textMarker.userid]; - var className = 'authorship-inline-' + author.color.substr(1); - var obj = { - from: textMarker.pos[0], - to: textMarker.pos[1] - }; - if (JSON.stringify(pos) == JSON.stringify(obj) && _textMarker.className && + if (havePendingOperation()) return + authorMarks = {} + for (let i = 0; i < authorship.length; i++) { + var atom = authorship[i] + let author = authors[atom[0]] + if (author) { + var prePos = editor.posFromIndex(atom[1]) + var preLine = editor.getLine(prePos.line) + var postPos = editor.posFromIndex(atom[2]) + var postLine = editor.getLine(postPos.line) + if (prePos.ch === 0 && postPos.ch === postLine.length) { + for (let j = prePos.line; j <= postPos.line; j++) { + if (editor.getLine(j)) { + authorMarks[j] = initMarkAndCheckGutter(authorMarks[j], author, atom[3]) + } + } + } else if (postPos.line - prePos.line >= 1) { + var startLine = prePos.line + var endLine = postPos.line + if (prePos.ch === preLine.length) { + startLine++ + } else if (prePos.ch !== 0) { + let mark = initMarkAndCheckGutter(authorMarks[prePos.line], author, atom[3]) + var _postPos = { + line: prePos.line, + ch: preLine.length + } + if (JSON.stringify(prePos) !== JSON.stringify(_postPos)) { + mark.textmarkers.push({ userid: author.userid, pos: [prePos, _postPos] }) + startLine++ + } + authorMarks[prePos.line] = mark + } + if (postPos.ch === 0) { + endLine-- + } else if (postPos.ch !== postLine.length) { + let mark = initMarkAndCheckGutter(authorMarks[postPos.line], author, atom[3]) + var _prePos = { + line: postPos.line, + ch: 0 + } + if (JSON.stringify(_prePos) !== JSON.stringify(postPos)) { + mark.textmarkers.push({ userid: author.userid, pos: [_prePos, postPos] }) + endLine-- + } + authorMarks[postPos.line] = mark + } + for (let j = startLine; j <= endLine; j++) { + if (editor.getLine(j)) { + authorMarks[j] = initMarkAndCheckGutter(authorMarks[j], author, atom[3]) + } + } + } else { + let mark = initMarkAndCheckGutter(authorMarks[prePos.line], author, atom[3]) + if (JSON.stringify(prePos) !== JSON.stringify(postPos)) { + mark.textmarkers.push({ + userid: author.userid, + pos: [prePos, postPos] + }) + } + authorMarks[prePos.line] = mark + } + } + } + addTextMarkers = [] + editor.eachLine(iterateLine) + var allTextMarks = editor.getAllMarks() + for (let i = 0; i < allTextMarks.length; i++) { + let _textMarker = allTextMarks[i] + var pos = _textMarker.find() + var found = false + for (let j = 0; j < addTextMarkers.length; j++) { + let textMarker = addTextMarkers[j] + let author = authors[textMarker.userid] + let className = 'authorship-inline-' + author.color.substr(1) + var obj = { + from: textMarker.pos[0], + to: textMarker.pos[1] + } + if (JSON.stringify(pos) === JSON.stringify(obj) && _textMarker.className && _textMarker.className.indexOf(className) > -1) { - addTextMarkers.splice(j, 1); - j--; - found = true; - break; - } - } - if (!found && _textMarker.className && _textMarker.className.indexOf('authorship-inline') > -1) { - _textMarker.clear(); - } - } - for (var i = 0; i < addTextMarkers.length; i++) { - var textMarker = addTextMarkers[i]; - var author = authors[textMarker.userid]; - var rgbcolor = hex2rgb(author.color); - var colorString = "rgba(" + rgbcolor.red + "," + rgbcolor.green + "," + rgbcolor.blue + ",0.7)"; - var styleString = textMarkderStylePrefix + colorString + textMarkderStylePostfix; - var className = 'authorship-inline-' + author.color.substr(1); - var rule = "." + className + "{" + styleString + "}"; - addStyleRule(rule); - var _textMarker = editor.markText(textMarker.pos[0], textMarker.pos[1], { - className: 'authorship-inline ' + className, - title: author.name - }); - } - authorshipMarks = authorMarks; -} -function iterateLine(line) { - var lineNumber = line.lineNo(); - var currMark = authorMarks[lineNumber]; - var author = currMark ? authors[currMark.gutter.userid] : null; - if (currMark && author) { - var className = 'authorship-gutter-' + author.color.substr(1); - var gutters = line.gutterMarkers; - if (!gutters || !gutters['authorship-gutters'] || + addTextMarkers.splice(j, 1) + j-- + found = true + break + } + } + if (!found && _textMarker.className && _textMarker.className.indexOf('authorship-inline') > -1) { + _textMarker.clear() + } + } + for (let i = 0; i < addTextMarkers.length; i++) { + let textMarker = addTextMarkers[i] + let author = authors[textMarker.userid] + var rgbcolor = hex2rgb(author.color) + var colorString = 'rgba(' + rgbcolor.red + ',' + rgbcolor.green + ',' + rgbcolor.blue + ',0.7)' + var styleString = textMarkderStylePrefix + colorString + textMarkderStylePostfix + let className = 'authorship-inline-' + author.color.substr(1) + var rule = '.' + className + '{' + styleString + '}' + addStyleRule(rule) + editor.markText(textMarker.pos[0], textMarker.pos[1], { + className: 'authorship-inline ' + className, + title: author.name + }) + } +} +function iterateLine (line) { + var lineNumber = line.lineNo() + var currMark = authorMarks[lineNumber] + var author = currMark ? authors[currMark.gutter.userid] : null + if (currMark && author) { + let className = 'authorship-gutter-' + author.color.substr(1) + var gutters = line.gutterMarkers + if (!gutters || !gutters['authorship-gutters'] || !gutters['authorship-gutters'].className || !gutters['authorship-gutters'].className.indexOf(className) < 0) { - var styleString = gutterStylePrefix + author.color + gutterStylePostfix; - var rule = "." + className + "{" + styleString + "}"; - addStyleRule(rule); - var gutter = $('<div>', { - class: 'authorship-gutter ' + className, - title: author.name - }); - editor.setGutterMarker(line, "authorship-gutters", gutter[0]); - } - } else { - editor.setGutterMarker(line, "authorship-gutters", null); - } - if (currMark && currMark.textmarkers.length > 0) { - for (var i = 0; i < currMark.textmarkers.length; i++) { - var textMarker = currMark.textmarkers[i]; - if (textMarker.userid != currMark.gutter.userid) { - addTextMarkers.push(textMarker); - } - } - } + var styleString = gutterStylePrefix + author.color + gutterStylePostfix + var rule = '.' + className + '{' + styleString + '}' + addStyleRule(rule) + var gutter = $('<div>', { + class: 'authorship-gutter ' + className, + title: author.name + }) + editor.setGutterMarker(line, 'authorship-gutters', gutter[0]) + } + } else { + editor.setGutterMarker(line, 'authorship-gutters', null) + } + if (currMark && currMark.textmarkers.length > 0) { + for (var i = 0; i < currMark.textmarkers.length; i++) { + let textMarker = currMark.textmarkers[i] + if (textMarker.userid !== currMark.gutter.userid) { + addTextMarkers.push(textMarker) + } + } + } } editor.on('update', function () { - $('.authorship-gutter:not([data-original-title])').tooltip({ - container: '.CodeMirror-lines', - placement: 'right', - delay: { "show": 500, "hide": 100 } - }); - $('.authorship-inline:not([data-original-title])').tooltip({ - container: '.CodeMirror-lines', - placement: 'bottom', - delay: { "show": 500, "hide": 100 } - }); + $('.authorship-gutter:not([data-original-title])').tooltip({ + container: '.CodeMirror-lines', + placement: 'right', + delay: { 'show': 500, 'hide': 100 } + }) + $('.authorship-inline:not([data-original-title])').tooltip({ + container: '.CodeMirror-lines', + placement: 'bottom', + delay: { 'show': 500, 'hide': 100 } + }) // clear tooltip which described element has been removed - $('[id^="tooltip"]').each(function (index, element) { - var $ele = $(element); - if ($('[aria-describedby="' + $ele.attr('id') + '"]').length <= 0) $ele.remove(); - }); -}); + $('[id^="tooltip"]').each(function (index, element) { + var $ele = $(element) + if ($('[aria-describedby="' + $ele.attr('id') + '"]').length <= 0) $ele.remove() + }) +}) socket.on('check', function (data) { - //console.log(data); - updateInfo(data); -}); + // console.log(data); + updateInfo(data) +}) socket.on('permission', function (data) { - updatePermission(data.permission); -}); -var docmaxlength = null; -var permission = null; + updatePermission(data.permission) +}) +var docmaxlength = null +var permission = null socket.on('refresh', function (data) { - //console.log(data); - docmaxlength = data.docmaxlength; - editor.setOption("maxLength", docmaxlength); - updateInfo(data); - updatePermission(data.permission); - if (!loaded) { + // console.log(data); + docmaxlength = data.docmaxlength + editor.setOption('maxLength', docmaxlength) + updateInfo(data) + updatePermission(data.permission) + if (!window.loaded) { // auto change mode if no content detected - var nocontent = editor.getValue().length <= 0; - if (nocontent) { - if (visibleXS) - currentMode = modeType.edit; - else - currentMode = modeType.both; - } + var nocontent = editor.getValue().length <= 0 + if (nocontent) { + if (window.visibleXS) { window.currentMode = modeType.edit } else { window.currentMode = modeType.both } + } // parse mode from url - if (window.location.search.length > 0) { - var urlMode = modeType[window.location.search.substr(1)]; - if (urlMode) currentMode = urlMode; - } - changeMode(currentMode); - if (nocontent && !visibleXS) { - editor.focus(); - editor.refresh(); - } - updateViewInner(); // bring up view rendering earlier - updateHistory(); //update history whether have content or not - loaded = true; - emitUserStatus(); //send first user status - updateOnlineStatus(); //update first online status - setTimeout(function () { - //work around editor not refresh or doc not fully loaded - windowResizeInner(); - //work around might not scroll to hash - scrollToHash(); - }, 1); - } - if (editor.getOption('readOnly')) - editor.setOption('readOnly', false); -}); - -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 setDoc = !cmClient || (cmClient && (cmClient.revision === -1 || (cmClient.revision !== obj.revision && !havePendingOperation()))) || obj.force; + if (window.location.search.length > 0) { + var urlMode = modeType[window.location.search.substr(1)] + if (urlMode) window.currentMode = urlMode + } + changeMode(window.currentMode) + if (nocontent && !window.visibleXS) { + editor.focus() + editor.refresh() + } + updateViewInner() // bring up view rendering earlier + updateHistory() // update history whether have content or not + window.loaded = true + emitUserStatus() // send first user status + updateOnlineStatus() // update first online status + setTimeout(function () { + // work around editor not refresh or doc not fully loaded + windowResizeInner() + // work around might not scroll to hash + scrollToHash() + }, 1) + } + if (editor.getOption('readOnly')) { editor.setOption('readOnly', false) } +}) - saveInfo(); - if (setDoc && bodyMismatch) { - if (cmClient) cmClient.editorAdapter.ignoreNextChange = true; - if (body) editor.setValue(body); - else editor.setValue(""); - } +var EditorClient = ot.EditorClient +var SocketIOAdapter = ot.SocketIOAdapter +var CodeMirrorAdapter = ot.CodeMirrorAdapter +var cmClient = null +var synchronized_ = null - if (!loaded) { - editor.clearHistory(); - ui.spinner.hide(); - ui.content.fadeIn(); - } else { - //if current doc is equal to the doc before disconnect - if (setDoc && bodyMismatch) editor.clearHistory(); - else if (lastInfo.history) editor.setHistory(lastInfo.history); - lastInfo.history = null; - } +function havePendingOperation () { + return !!((cmClient && cmClient.state && cmClient.state.hasOwnProperty('outstanding'))) +} - if (!cmClient) { - cmClient = window.cmClient = new EditorClient( +socket.on('doc', function (obj) { + var body = obj.str + var bodyMismatch = editor.getValue() !== body + var setDoc = !cmClient || (cmClient && (cmClient.revision === -1 || (cmClient.revision !== obj.revision && !havePendingOperation()))) || obj.force + + saveInfo() + if (setDoc && bodyMismatch) { + if (cmClient) cmClient.editorAdapter.ignoreNextChange = true + if (body) editor.setValue(body) + else editor.setValue('') + } + + if (!window.loaded) { + editor.clearHistory() + ui.spinner.hide() + ui.content.fadeIn() + } else { + // if current doc is equal to the doc before disconnect + if (setDoc && bodyMismatch) editor.clearHistory() + else if (window.lastInfo.history) editor.setHistory(window.lastInfo.history) + window.lastInfo.history = null + } + + if (!cmClient) { + cmClient = window.cmClient = new EditorClient( 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(synchronized_); - cmClient.initializeClientList(); - cmClient.initializeClients(obj.clients); - } else if (havePendingOperation()) { - cmClient.serverReconnect(); - } - - if (setDoc && bodyMismatch) { - isDirty = true; - updateView(); - } - - restoreInfo(); -}); + ) + synchronized_ = cmClient.state + } else if (setDoc) { + if (bodyMismatch) { + cmClient.undoManager.undoStack.length = 0 + cmClient.undoManager.redoStack.length = 0 + } + cmClient.revision = obj.revision + cmClient.setState(synchronized_) + cmClient.initializeClientList() + cmClient.initializeClients(obj.clients) + } else if (havePendingOperation()) { + cmClient.serverReconnect() + } + + if (setDoc && bodyMismatch) { + window.isDirty = true + updateView() + } + + restoreInfo() +}) socket.on('ack', function () { - isDirty = true; - updateView(); -}); + window.isDirty = true + updateView() +}) socket.on('operation', function () { - isDirty = true; - updateView(); -}); + window.isDirty = true + updateView() +}) socket.on('online users', function (data) { - if (debug) - console.debug(data); - onlineUsers = data.users; - updateOnlineStatus(); - $('.CodeMirror-other-cursors').children().each(function (key, value) { - var found = false; - for (var i = 0; i < data.users.length; i++) { - var user = data.users[i]; - if ($(this).attr('id') == user.id) - found = true; - } - if (!found) - $(this).stop(true).fadeOut("normal", function () { - $(this).remove(); - }); - }); + if (debug) { console.debug(data) } + window.onlineUsers = data.users + updateOnlineStatus() + $('.CodeMirror-other-cursors').children().each(function (key, value) { + var found = false for (var i = 0; i < data.users.length; i++) { - var user = data.users[i]; - if (user.id != socket.id) - buildCursor(user); - else - personalInfo = user; - } -}); + var user = data.users[i] + if ($(this).attr('id') === user.id) { found = true } + } + if (!found) { + $(this).stop(true).fadeOut('normal', function () { + $(this).remove() + }) + } + }) + for (var i = 0; i < data.users.length; i++) { + var user = data.users[i] + if (user.id !== socket.id) { buildCursor(user) } else { window.personalInfo = user } + } +}) socket.on('user status', function (data) { - if (debug) - console.debug(data); - for (var i = 0; i < onlineUsers.length; i++) { - if (onlineUsers[i].id == data.id) { - onlineUsers[i] = data; - } - } - updateOnlineStatus(); - if (data.id != socket.id) - buildCursor(data); -}); + if (debug) { console.debug(data) } + for (var i = 0; i < window.onlineUsers.length; i++) { + if (window.onlineUsers[i].id === data.id) { + window.onlineUsers[i] = data + } + } + updateOnlineStatus() + if (data.id !== socket.id) { buildCursor(data) } +}) socket.on('cursor focus', function (data) { - if (debug) - console.debug(data); - for (var i = 0; i < onlineUsers.length; i++) { - if (onlineUsers[i].id == data.id) { - onlineUsers[i].cursor = data.cursor; - } - } - if (data.id != socket.id) - buildCursor(data); - //force show - var cursor = $('div[data-clientid="' + data.id + '"]'); - if (cursor.length > 0) { - cursor.stop(true).fadeIn(); - } -}); + if (debug) { console.debug(data) } + for (var i = 0; i < window.onlineUsers.length; i++) { + if (window.onlineUsers[i].id === data.id) { + window.onlineUsers[i].cursor = data.cursor + } + } + if (data.id !== socket.id) { buildCursor(data) } + // force show + var cursor = $('div[data-clientid="' + data.id + '"]') + if (cursor.length > 0) { + cursor.stop(true).fadeIn() + } +}) socket.on('cursor activity', function (data) { - if (debug) - console.debug(data); - for (var i = 0; i < onlineUsers.length; i++) { - if (onlineUsers[i].id == data.id) { - onlineUsers[i].cursor = data.cursor; - } + if (debug) { console.debug(data) } + for (var i = 0; i < window.onlineUsers.length; i++) { + if (window.onlineUsers[i].id === data.id) { + window.onlineUsers[i].cursor = data.cursor } - if (data.id != socket.id) - buildCursor(data); -}); + } + if (data.id !== socket.id) { buildCursor(data) } +}) socket.on('cursor blur', function (data) { - if (debug) - console.debug(data); - for (var i = 0; i < onlineUsers.length; i++) { - if (onlineUsers[i].id == data.id) { - onlineUsers[i].cursor = null; - } - } - if (data.id != socket.id) - buildCursor(data); - //force hide - var cursor = $('div[data-clientid="' + data.id + '"]'); - if (cursor.length > 0) { - cursor.stop(true).fadeOut(); - } -}); + if (debug) { console.debug(data) } + for (var i = 0; i < window.onlineUsers.length; i++) { + if (window.onlineUsers[i].id === data.id) { + window.onlineUsers[i].cursor = null + } + } + if (data.id !== socket.id) { buildCursor(data) } + // force hide + var cursor = $('div[data-clientid="' + data.id + '"]') + if (cursor.length > 0) { + cursor.stop(true).fadeOut() + } +}) var options = { - valueNames: ['id', 'name'], - item: '<li class="ui-user-item">\ - <span class="id" style="display:none;"></span>\ - <a href="#">\ - <span class="pull-left"><i class="ui-user-icon"></i></span><span class="ui-user-name name"></span><span class="pull-right"><i class="fa fa-circle ui-user-status"></i></span>\ - </a>\ - </li>' -}; -var onlineUserList = new List('online-user-list', options); -var shortOnlineUserList = new List('short-online-user-list', options); - -function updateOnlineStatus() { - if (!loaded || !socket.connected) return; - var _onlineUsers = deduplicateOnlineUsers(onlineUsers); - showStatus(statusType.online, _onlineUsers.length); - var items = onlineUserList.items; - //update or remove current list items - for (var i = 0; i < items.length; i++) { - var found = false; - var foundindex = null; - for (var j = 0; j < _onlineUsers.length; j++) { - if (items[i].values().id == _onlineUsers[j].id) { - foundindex = j; - found = true; - break; - } - } - var id = items[i].values().id; - if (found) { - onlineUserList.get('id', id)[0].values(_onlineUsers[foundindex]); - shortOnlineUserList.get('id', id)[0].values(_onlineUsers[foundindex]); - } else { - onlineUserList.remove('id', id); - shortOnlineUserList.remove('id', id); - } - } - //add not in list items - for (var i = 0; i < _onlineUsers.length; i++) { - var found = false; - for (var j = 0; j < items.length; j++) { - if (items[j].values().id == _onlineUsers[i].id) { - found = true; - break; - } - } - if (!found) { - onlineUserList.add(_onlineUsers[i]); - shortOnlineUserList.add(_onlineUsers[i]); - } + valueNames: ['id', 'name'], + item: '<li class="ui-user-item">' + + '<span class="id" style="display:none;"></span>' + + '<a href="#">' + + '<span class="pull-left"><i class="ui-user-icon"></i></span><span class="ui-user-name name"></span><span class="pull-right"><i class="fa fa-circle ui-user-status"></i></span>' + + '</a>' + + '</li>' +} +var onlineUserList = new List('online-user-list', options) +var shortOnlineUserList = new List('short-online-user-list', options) + +function updateOnlineStatus () { + if (!window.loaded || !socket.connected) return + var _onlineUsers = deduplicateOnlineUsers(window.onlineUsers) + showStatus(statusType.online, _onlineUsers.length) + var items = onlineUserList.items + // update or remove current list items + for (let i = 0; i < items.length; i++) { + let found = false + let foundindex = null + for (let j = 0; j < _onlineUsers.length; j++) { + if (items[i].values().id === _onlineUsers[j].id) { + foundindex = j + found = true + break + } + } + let id = items[i].values().id + if (found) { + onlineUserList.get('id', id)[0].values(_onlineUsers[foundindex]) + shortOnlineUserList.get('id', id)[0].values(_onlineUsers[foundindex]) + } else { + onlineUserList.remove('id', id) + shortOnlineUserList.remove('id', id) + } + } + // add not in list items + for (let i = 0; i < _onlineUsers.length; i++) { + let found = false + for (let j = 0; j < items.length; j++) { + if (items[j].values().id === _onlineUsers[i].id) { + found = true + break + } + } + if (!found) { + onlineUserList.add(_onlineUsers[i]) + shortOnlineUserList.add(_onlineUsers[i]) + } + } + // sorting + sortOnlineUserList(onlineUserList) + sortOnlineUserList(shortOnlineUserList) + // render list items + renderUserStatusList(onlineUserList) + renderUserStatusList(shortOnlineUserList) +} + +function sortOnlineUserList (list) { + // sort order by isSelf, login state, idle state, alphabet name, color brightness + list.sort('', { + sortFunction: function (a, b) { + var usera = a.values() + var userb = b.values() + var useraIsSelf = (usera.id === window.personalInfo.id || (usera.login && usera.userid === window.personalInfo.userid)) + var userbIsSelf = (userb.id === window.personalInfo.id || (userb.login && userb.userid === window.personalInfo.userid)) + if (useraIsSelf && !userbIsSelf) { + return -1 + } else if (!useraIsSelf && userbIsSelf) { + return 1 + } else { + if (usera.login && !userb.login) { return -1 } else if (!usera.login && userb.login) { return 1 } else { + if (!usera.idle && userb.idle) { return -1 } else if (usera.idle && !userb.idle) { return 1 } else { + if (usera.name && userb.name && usera.name.toLowerCase() < userb.name.toLowerCase()) { + return -1 + } else if (usera.name && userb.name && usera.name.toLowerCase() > userb.name.toLowerCase()) { return 1 } else { if (usera.color && userb.color && usera.color.toLowerCase() < userb.color.toLowerCase()) { return -1 } else if (usera.color && userb.color && usera.color.toLowerCase() > userb.color.toLowerCase()) { return 1 } else { return 0 } } + } + } + } + } + }) +} + +function renderUserStatusList (list) { + var items = list.items + for (var j = 0; j < items.length; j++) { + var item = items[j] + var userstatus = $(item.elm).find('.ui-user-status') + var usericon = $(item.elm).find('.ui-user-icon') + if (item.values().login && item.values().photo) { + usericon.css('background-image', 'url(' + item.values().photo + ')') + // add 1px more to right, make it feel aligned + usericon.css('margin-right', '6px') + $(item.elm).css('border-left', '4px solid ' + item.values().color) + usericon.css('margin-left', '-4px') + } else { + usericon.css('background-color', item.values().color) } - //sorting - sortOnlineUserList(onlineUserList); - sortOnlineUserList(shortOnlineUserList); - //render list items - renderUserStatusList(onlineUserList); - renderUserStatusList(shortOnlineUserList); -} - -function sortOnlineUserList(list) { - //sort order by isSelf, login state, idle state, alphabet name, color brightness - list.sort('', { - sortFunction: function (a, b) { - var usera = a.values(); - var userb = b.values(); - var useraIsSelf = (usera.id == personalInfo.id || (usera.login && usera.userid == personalInfo.userid)); - var userbIsSelf = (userb.id == personalInfo.id || (userb.login && userb.userid == personalInfo.userid)); - if (useraIsSelf && !userbIsSelf) { - return -1; - } else if (!useraIsSelf && userbIsSelf) { - return 1; - } else { - if (usera.login && !userb.login) - return -1; - else if (!usera.login && userb.login) - return 1; - else { - if (!usera.idle && userb.idle) - return -1; - else if (usera.idle && !userb.idle) - return 1; - else { - if (usera.name && userb.name && usera.name.toLowerCase() < userb.name.toLowerCase()) { - return -1; - } else if (usera.name && userb.name && usera.name.toLowerCase() > userb.name.toLowerCase()) { - return 1; - } else { - if (usera.color && userb.color && usera.color.toLowerCase() < userb.color.toLowerCase()) - return -1; - else if (usera.color && userb.color && usera.color.toLowerCase() > userb.color.toLowerCase()) - return 1; - else - return 0; - } - } - } - } - } - }); -} - -function renderUserStatusList(list) { - var items = list.items; - for (var j = 0; j < items.length; j++) { - var item = items[j]; - var userstatus = $(item.elm).find('.ui-user-status'); - var usericon = $(item.elm).find('.ui-user-icon'); - if (item.values().login && item.values().photo) { - usericon.css('background-image', 'url(' + item.values().photo + ')'); - //add 1px more to right, make it feel aligned - usericon.css('margin-right', '6px'); - $(item.elm).css('border-left', '4px solid ' + item.values().color); - usericon.css('margin-left', '-4px'); - } else { - usericon.css('background-color', item.values().color); - } - userstatus.removeClass('ui-user-status-offline ui-user-status-online ui-user-status-idle'); - if (item.values().idle) - userstatus.addClass('ui-user-status-idle'); - else - userstatus.addClass('ui-user-status-online'); - } -} - -function deduplicateOnlineUsers(list) { - var _onlineUsers = []; - for (var i = 0; i < list.length; i++) { - var user = $.extend({}, list[i]); - if (!user.userid) - _onlineUsers.push(user); - else { - var found = false; - for (var j = 0; j < _onlineUsers.length; j++) { - if (_onlineUsers[j].userid == user.userid) { - //keep self color when login - if (user.id == personalInfo.id) { - _onlineUsers[j].color = user.color; - } - //keep idle state if any of self client not idle - if (!user.idle) { - _onlineUsers[j].idle = user.idle; - _onlineUsers[j].color = user.color; - } - found = true; - break; - } - } - if (!found) - _onlineUsers.push(user); + userstatus.removeClass('ui-user-status-offline ui-user-status-online ui-user-status-idle') + if (item.values().idle) { userstatus.addClass('ui-user-status-idle') } else { userstatus.addClass('ui-user-status-online') } + } +} + +function deduplicateOnlineUsers (list) { + var _onlineUsers = [] + for (var i = 0; i < list.length; i++) { + var user = $.extend({}, list[i]) + if (!user.userid) { _onlineUsers.push(user) } else { + var found = false + for (var j = 0; j < _onlineUsers.length; j++) { + if (_onlineUsers[j].userid === user.userid) { + // keep self color when login + if (user.id === window.personalInfo.id) { + _onlineUsers[j].color = user.color + } + // keep idle state if any of self client not idle + if (!user.idle) { + _onlineUsers[j].idle = user.idle + _onlineUsers[j].color = user.color + } + found = true + break } + } + if (!found) { _onlineUsers.push(user) } } - return _onlineUsers; + } + return _onlineUsers } -var userStatusCache = null; +var userStatusCache = null -function emitUserStatus(force) { - if (!loaded) return; - var type = null; - if (visibleXS) - type = 'xs'; - else if (visibleSM) - type = 'sm'; - else if (visibleMD) - type = 'md'; - else if (visibleLG) - type = 'lg'; +function emitUserStatus (force) { + if (!window.loaded) return + var type = null + if (window.visibleXS) { type = 'xs' } else if (window.visibleSM) { type = 'sm' } else if (window.visibleMD) { type = 'md' } else if (window.visibleLG) { type = 'lg' } - personalInfo['idle'] = idle.isAway; - personalInfo['type'] = type; + window.personalInfo['idle'] = idle.isAway + window.personalInfo['type'] = type - for (var i = 0; i < onlineUsers.length; i++) { - if (onlineUsers[i].id == personalInfo.id) { - onlineUsers[i] = personalInfo; - } + for (var i = 0; i < window.onlineUsers.length; i++) { + if (window.onlineUsers[i].id === window.personalInfo.id) { + window.onlineUsers[i] = window.personalInfo } + } - var userStatus = { - idle: idle.isAway, - type: type - }; + var userStatus = { + idle: idle.isAway, + type: type + } - if (force || JSON.stringify(userStatus) != JSON.stringify(userStatusCache)) { - socket.emit('user status', userStatus); - userStatusCache = userStatus; - } + if (force || JSON.stringify(userStatus) !== JSON.stringify(userStatusCache)) { + socket.emit('user status', userStatus) + userStatusCache = userStatus + } } -function checkCursorTag(coord, ele) { - if (!ele) return; // return if element not exists +function checkCursorTag (coord, ele) { + if (!ele) return // return if element not exists // set margin - var tagRightMargin = 0; - var tagBottomMargin = 2; + var tagRightMargin = 0 + var tagBottomMargin = 2 // use sizer to get the real doc size (won't count status bar and gutters) - var docWidth = ui.area.codemirrorSizer.width(); - var docHeight = ui.area.codemirrorSizer.height(); + var docWidth = ui.area.codemirrorSizer.width() // get editor size (status bar not count in) - var editorWidth = ui.area.codemirror.width(); - var editorHeight = ui.area.codemirror.height(); + var editorHeight = ui.area.codemirror.height() // get element size - var width = ele.outerWidth(); - var height = ele.outerHeight(); - var padding = (ele.outerWidth() - ele.width()) / 2; + var width = ele.outerWidth() + var height = ele.outerHeight() + var padding = (ele.outerWidth() - ele.width()) / 2 // get coord position - var left = coord.left; - var top = coord.top; + var left = coord.left + var top = coord.top // get doc top offset (to workaround with viewport) - var docTopOffset = ui.area.codemirrorSizerInner.position().top; + var docTopOffset = ui.area.codemirrorSizerInner.position().top // set offset - var offsetLeft = -3; - var offsetTop = defaultTextHeight; + var offsetLeft = -3 + var offsetTop = defaultTextHeight // only do when have width and height - if (width > 0 && height > 0) { + if (width > 0 && height > 0) { // flip x when element right bound larger than doc width - if (left + width + offsetLeft + tagRightMargin > docWidth) { - offsetLeft = -(width + tagRightMargin) + padding + offsetLeft; - } + if (left + width + offsetLeft + tagRightMargin > docWidth) { + offsetLeft = -(width + tagRightMargin) + padding + offsetLeft + } // flip y when element bottom bound larger than doc height // and element top position is larger than element height - if (top + docTopOffset + height + offsetTop + tagBottomMargin > Math.max(editor.doc.height, editorHeight) && top + docTopOffset > height + tagBottomMargin) { - offsetTop = -(height); - } + if (top + docTopOffset + height + offsetTop + tagBottomMargin > Math.max(editor.doc.height, editorHeight) && top + docTopOffset > height + tagBottomMargin) { + offsetTop = -(height) } + } // set position - ele[0].style.left = offsetLeft + 'px'; - ele[0].style.top = offsetTop + 'px'; -} - -function buildCursor(user) { - if (currentMode == modeType.view) return; - if (!user.cursor) return; - var coord = editor.charCoords(user.cursor, 'windows'); - coord.left = coord.left < 4 ? 4 : coord.left; - coord.top = coord.top < 0 ? 0 : coord.top; - var iconClass = 'fa-user'; - switch (user.type) { - case 'xs': - iconClass = 'fa-mobile'; - break; - case 'sm': - iconClass = 'fa-tablet'; - break; - case 'md': - iconClass = 'fa-desktop'; - break; - case 'lg': - iconClass = 'fa-desktop'; - break; - } - if ($('.CodeMirror-other-cursors').length <= 0) { - $("<div class='CodeMirror-other-cursors'>").insertAfter('.CodeMirror-cursors'); - } - if ($('div[data-clientid="' + user.id + '"]').length <= 0) { - var cursor = $('<div data-clientid="' + user.id + '" class="CodeMirror-other-cursor" style="display:none;"></div>'); - cursor.attr('data-line', user.cursor.line); - cursor.attr('data-ch', user.cursor.ch); - cursor.attr('data-offset-left', 0); - cursor.attr('data-offset-top', 0); - - var cursorbar = $('<div class="cursorbar"> </div>'); - cursorbar[0].style.height = defaultTextHeight + 'px'; - cursorbar[0].style.borderLeft = '2px solid ' + user.color; - - var icon = '<i class="fa ' + iconClass + '"></i>'; - - var cursortag = $('<div class="cursortag">' + icon + ' <span class="name">' + user.name + '</span></div>'); - //cursortag[0].style.background = color; - cursortag[0].style.color = user.color; - - cursor.attr('data-mode', 'hover'); - cursortag.delay(2000).fadeOut("fast"); - cursor.hover( + ele[0].style.left = offsetLeft + 'px' + ele[0].style.top = offsetTop + 'px' +} + +function buildCursor (user) { + if (window.currentMode === modeType.view) return + if (!user.cursor) return + var coord = editor.charCoords(user.cursor, 'windows') + coord.left = coord.left < 4 ? 4 : coord.left + coord.top = coord.top < 0 ? 0 : coord.top + var iconClass = 'fa-user' + switch (user.type) { + case 'xs': + iconClass = 'fa-mobile' + break + case 'sm': + iconClass = 'fa-tablet' + break + case 'md': + iconClass = 'fa-desktop' + break + case 'lg': + iconClass = 'fa-desktop' + break + } + if ($('.CodeMirror-other-cursors').length <= 0) { + $("<div class='CodeMirror-other-cursors'>").insertAfter('.CodeMirror-cursors') + } + if ($('div[data-clientid="' + user.id + '"]').length <= 0) { + let cursor = $('<div data-clientid="' + user.id + '" class="CodeMirror-other-cursor" style="display:none;"></div>') + cursor.attr('data-line', user.cursor.line) + cursor.attr('data-ch', user.cursor.ch) + cursor.attr('data-offset-left', 0) + cursor.attr('data-offset-top', 0) + + let cursorbar = $('<div class="cursorbar"> </div>') + cursorbar[0].style.height = defaultTextHeight + 'px' + cursorbar[0].style.borderLeft = '2px solid ' + user.color + + var icon = '<i class="fa ' + iconClass + '"></i>' + + let cursortag = $('<div class="cursortag">' + icon + ' <span class="name">' + user.name + '</span></div>') + // cursortag[0].style.background = color; + cursortag[0].style.color = user.color + + cursor.attr('data-mode', 'hover') + cursortag.delay(2000).fadeOut('fast') + cursor.hover( function () { - if (cursor.attr('data-mode') == 'hover') - cursortag.stop(true).fadeIn("fast"); + if (cursor.attr('data-mode') === 'hover') { cursortag.stop(true).fadeIn('fast') } }, function () { - if (cursor.attr('data-mode') == 'hover') - cursortag.stop(true).fadeOut("fast"); - }); - - function switchMode(ele) { - if (ele.attr('data-mode') == 'state') - ele.attr('data-mode', 'hover'); - else if (ele.attr('data-mode') == 'hover') - ele.attr('data-mode', 'state'); - } - - function switchTag(ele) { - if (ele.css('display') === 'none') - ele.stop(true).fadeIn("fast"); - else - ele.stop(true).fadeOut("fast"); - } - var hideCursorTagDelay = 2000; - var hideCursorTagTimer = null; - - function hideCursorTag() { - if (cursor.attr('data-mode') == 'hover') - cursortag.fadeOut("fast"); - } - cursor.on('touchstart', function (e) { - var display = cursortag.css('display'); - cursortag.stop(true).fadeIn("fast"); - clearTimeout(hideCursorTagTimer); - hideCursorTagTimer = setTimeout(hideCursorTag, hideCursorTagDelay); - if (display === 'none') { - e.preventDefault(); - e.stopPropagation(); - } - }); - cursortag.on('mousedown touchstart', function (e) { - if (cursor.attr('data-mode') == 'state') - switchTag(cursortag); - switchMode(cursor); - e.preventDefault(); - e.stopPropagation(); - }); - - cursor.append(cursorbar); - cursor.append(cursortag); - - cursor[0].style.left = coord.left + 'px'; - cursor[0].style.top = coord.top + 'px'; - $('.CodeMirror-other-cursors').append(cursor); - - if (!user.idle) - cursor.stop(true).fadeIn(); - - checkCursorTag(coord, cursortag); - } else { - var cursor = $('div[data-clientid="' + user.id + '"]'); - var lineDiff = Math.abs(cursor.attr('data-line') - user.cursor.line); - cursor.attr('data-line', user.cursor.line); - cursor.attr('data-ch', user.cursor.ch); - - var cursorbar = cursor.find('.cursorbar'); - cursorbar[0].style.height = defaultTextHeight + 'px'; - cursorbar[0].style.borderLeft = '2px solid ' + user.color; - - var cursortag = cursor.find('.cursortag'); - cursortag.find('i').removeClass().addClass('fa').addClass(iconClass); - cursortag.find(".name").text(user.name); - - if (cursor.css('display') === 'none') { - cursor[0].style.left = coord.left + 'px'; - cursor[0].style.top = coord.top + 'px'; - } else { - cursor.animate({ - "left": coord.left, - "top": coord.top - }, { - duration: cursorAnimatePeriod, - queue: false - }); - } + if (cursor.attr('data-mode') === 'hover') { cursortag.stop(true).fadeOut('fast') } + }) - if (user.idle && cursor.css('display') !== 'none') - cursor.stop(true).fadeOut(); - else if (!user.idle && cursor.css('display') === 'none') - cursor.stop(true).fadeIn(); + var hideCursorTagDelay = 2000 + var hideCursorTagTimer = null - checkCursorTag(coord, cursortag); + var switchMode = function (ele) { + if (ele.attr('data-mode') === 'state') { ele.attr('data-mode', 'hover') } else if (ele.attr('data-mode') === 'hover') { ele.attr('data-mode', 'state') } } -} -//editor actions -function removeNullByte(cm, change) { - var str = change.text.join("\n"); - if (/\u0000/g.test(str) && change.update) { - change.update(change.from, change.to, str.replace(/\u0000/g, "").split("\n")); + var switchTag = function (ele) { + if (ele.css('display') === 'none') { ele.stop(true).fadeIn('fast') } else { ele.stop(true).fadeOut('fast') } } -} -function enforceMaxLength(cm, change) { - var maxLength = cm.getOption("maxLength"); - if (maxLength && change.update) { - var str = change.text.join("\n"); - var delta = str.length - (cm.indexFromPos(change.to) - cm.indexFromPos(change.from)); - if (delta <= 0) { - return false; - } - delta = cm.getValue().length + delta - maxLength; - if (delta > 0) { - str = str.substr(0, str.length - delta); - change.update(change.from, change.to, str.split("\n")); - return true; - } + + var hideCursorTag = function () { + if (cursor.attr('data-mode') === 'hover') { cursortag.fadeOut('fast') } } - return false; -} -var ignoreEmitEvents = ['setValue', 'ignoreHistory']; -editor.on('beforeChange', function (cm, change) { - if (debug) - console.debug(change); - removeNullByte(cm, change); - if (enforceMaxLength(cm, change)) { - $('.limit-modal').modal('show'); - } - var isIgnoreEmitEvent = (ignoreEmitEvents.indexOf(change.origin) != -1); - if (!isIgnoreEmitEvent) { - if (!havePermission()) { - change.canceled = true; - switch (permission) { - case "editable": - $('.signin-modal').modal('show'); - break; - case "locked": - case "private": - $('.locked-modal').modal('show'); - break; - } - } + cursor.on('touchstart', function (e) { + var display = cursortag.css('display') + cursortag.stop(true).fadeIn('fast') + clearTimeout(hideCursorTagTimer) + hideCursorTagTimer = setTimeout(hideCursorTag, hideCursorTagDelay) + if (display === 'none') { + e.preventDefault() + e.stopPropagation() + } + }) + cursortag.on('mousedown touchstart', function (e) { + if (cursor.attr('data-mode') === 'state') { switchTag(cursortag) } + switchMode(cursor) + e.preventDefault() + e.stopPropagation() + }) + + cursor.append(cursorbar) + cursor.append(cursortag) + + cursor[0].style.left = coord.left + 'px' + cursor[0].style.top = coord.top + 'px' + $('.CodeMirror-other-cursors').append(cursor) + + if (!user.idle) { cursor.stop(true).fadeIn() } + + checkCursorTag(coord, cursortag) + } else { + let cursor = $('div[data-clientid="' + user.id + '"]') + cursor.attr('data-line', user.cursor.line) + cursor.attr('data-ch', user.cursor.ch) + + let cursorbar = cursor.find('.cursorbar') + cursorbar[0].style.height = defaultTextHeight + 'px' + cursorbar[0].style.borderLeft = '2px solid ' + user.color + + let cursortag = cursor.find('.cursortag') + cursortag.find('i').removeClass().addClass('fa').addClass(iconClass) + cursortag.find('.name').text(user.name) + + if (cursor.css('display') === 'none') { + cursor[0].style.left = coord.left + 'px' + cursor[0].style.top = coord.top + 'px' } else { - if (change.origin == 'ignoreHistory') { - setHaveUnreadChanges(true); - updateTitleReminder(); - } - } - if (cmClient && !socket.connected) - cmClient.editorAdapter.ignoreNextChange = true; -}); + cursor.animate({ + 'left': coord.left, + 'top': coord.top + }, { + duration: cursorAnimatePeriod, + queue: false + }) + } + + if (user.idle && cursor.css('display') !== 'none') { cursor.stop(true).fadeOut() } else if (!user.idle && cursor.css('display') === 'none') { cursor.stop(true).fadeIn() } + + checkCursorTag(coord, cursortag) + } +} + +// editor actions +function removeNullByte (cm, change) { + var str = change.text.join('\n') + if (/\u0000/g.test(str) && change.update) { + change.update(change.from, change.to, str.replace(/\u0000/g, '').split('\n')) + } +} +function enforceMaxLength (cm, change) { + var maxLength = cm.getOption('maxLength') + if (maxLength && change.update) { + var str = change.text.join('\n') + var delta = str.length - (cm.indexFromPos(change.to) - cm.indexFromPos(change.from)) + if (delta <= 0) { + return false + } + delta = cm.getValue().length + delta - maxLength + if (delta > 0) { + str = str.substr(0, str.length - delta) + change.update(change.from, change.to, str.split('\n')) + return true + } + } + return false +} +var ignoreEmitEvents = ['setValue', 'ignoreHistory'] +editor.on('beforeChange', function (cm, change) { + if (debug) { console.debug(change) } + removeNullByte(cm, change) + if (enforceMaxLength(cm, change)) { + $('.limit-modal').modal('show') + } + var isIgnoreEmitEvent = (ignoreEmitEvents.indexOf(change.origin) !== -1) + if (!isIgnoreEmitEvent) { + if (!havePermission()) { + change.canceled = true + switch (permission) { + case 'editable': + $('.signin-modal').modal('show') + break + case 'locked': + case 'private': + $('.locked-modal').modal('show') + break + } + } + } else { + if (change.origin === 'ignoreHistory') { + setHaveUnreadChanges(true) + updateTitleReminder() + } + } + if (cmClient && !socket.connected) { cmClient.editorAdapter.ignoreNextChange = true } +}) editor.on('cut', function () { - //na -}); + // na +}) editor.on('paste', function () { - //na -}); + // na +}) editor.on('changes', function (cm, changes) { - updateHistory(); - var docLength = editor.getValue().length; - //workaround for big documents - var newViewportMargin = 20; - if (docLength > 20000) { - newViewportMargin = 1; - } else if (docLength > 10000) { - newViewportMargin = 10; - } else if (docLength > 5000) { - newViewportMargin = 15; - } - if (newViewportMargin != viewportMargin) { - viewportMargin = newViewportMargin; - windowResize(); - } - checkEditorScrollbar(); - if (ui.area.codemirrorScroll[0].scrollHeight > ui.area.view[0].scrollHeight && editorHasFocus()) { - postUpdateEvent = function () { - syncScrollToView(); - postUpdateEvent = null; - }; - } -}); + updateHistory() + var docLength = editor.getValue().length + // workaround for big documents + var newViewportMargin = 20 + if (docLength > 20000) { + newViewportMargin = 1 + } else if (docLength > 10000) { + newViewportMargin = 10 + } else if (docLength > 5000) { + newViewportMargin = 15 + } + if (newViewportMargin !== viewportMargin) { + viewportMargin = newViewportMargin + windowResize() + } + checkEditorScrollbar() + if (ui.area.codemirrorScroll[0].scrollHeight > ui.area.view[0].scrollHeight && editorHasFocus()) { + postUpdateEvent = function () { + syncScrollToView() + postUpdateEvent = null + } + } +}) editor.on('focus', function (cm) { - for (var i = 0; i < onlineUsers.length; i++) { - if (onlineUsers[i].id == personalInfo.id) { - onlineUsers[i].cursor = editor.getCursor(); - } + for (var i = 0; i < window.onlineUsers.length; i++) { + if (window.onlineUsers[i].id === window.personalInfo.id) { + window.onlineUsers[i].cursor = editor.getCursor() } - personalInfo['cursor'] = editor.getCursor(); - socket.emit('cursor focus', editor.getCursor()); -}); + } + window.personalInfo['cursor'] = editor.getCursor() + socket.emit('cursor focus', editor.getCursor()) +}) editor.on('cursorActivity', function (cm) { - updateStatusBar(); - cursorActivity(); -}); + updateStatusBar() + cursorActivity() +}) editor.on('beforeSelectionChange', function (doc, selections) { - if (selections) - selection = selections.ranges[0]; - else - selection = null; - updateStatusBar(); -}); - -var cursorActivity = _.debounce(cursorActivityInner, cursorActivityDebounce); - -function cursorActivityInner() { - if (editorHasFocus() && !Visibility.hidden()) { - for (var i = 0; i < onlineUsers.length; i++) { - if (onlineUsers[i].id == personalInfo.id) { - onlineUsers[i].cursor = editor.getCursor(); - } - } - personalInfo['cursor'] = editor.getCursor(); - socket.emit('cursor activity', editor.getCursor()); + if (selections) { selection = selections.ranges[0] } else { selection = null } + updateStatusBar() +}) + +var cursorActivity = _.debounce(cursorActivityInner, cursorActivityDebounce) + +function cursorActivityInner () { + if (editorHasFocus() && !Visibility.hidden()) { + for (var i = 0; i < window.onlineUsers.length; i++) { + if (window.onlineUsers[i].id === window.personalInfo.id) { + window.onlineUsers[i].cursor = editor.getCursor() + } } + window.personalInfo['cursor'] = editor.getCursor() + socket.emit('cursor activity', editor.getCursor()) + } } editor.on('blur', function (cm) { - for (var i = 0; i < onlineUsers.length; i++) { - if (onlineUsers[i].id == personalInfo.id) { - onlineUsers[i].cursor = null; - } + for (var i = 0; i < window.onlineUsers.length; i++) { + if (window.onlineUsers[i].id === window.personalInfo.id) { + window.onlineUsers[i].cursor = null } - personalInfo['cursor'] = null; - socket.emit('cursor blur'); -}); - -function saveInfo() { - var scrollbarStyle = editor.getOption('scrollbarStyle'); - var left = $(window).scrollLeft(); - var top = $(window).scrollTop(); - switch (currentMode) { - case modeType.edit: - if (scrollbarStyle == 'native') { - lastInfo.edit.scroll.left = left; - lastInfo.edit.scroll.top = top; - } else { - lastInfo.edit.scroll = editor.getScrollInfo(); - } - break; - case modeType.view: - lastInfo.view.scroll.left = left; - lastInfo.view.scroll.top = top; - break; - case modeType.both: - lastInfo.edit.scroll = editor.getScrollInfo(); - lastInfo.view.scroll.left = ui.area.view.scrollLeft(); - lastInfo.view.scroll.top = ui.area.view.scrollTop(); - break; - } - lastInfo.edit.cursor = editor.getCursor(); - lastInfo.edit.selections = editor.listSelections(); - lastInfo.needRestore = true; -} - -function restoreInfo() { - var scrollbarStyle = editor.getOption('scrollbarStyle'); - if (lastInfo.needRestore) { - 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') { - $(window).scrollLeft(lastInfo.edit.scroll.left); - $(window).scrollTop(lastInfo.edit.scroll.top); - } else { - var left = lastInfo.edit.scroll.left; - var top = lastInfo.edit.scroll.top; - editor.scrollIntoView(); - editor.scrollTo(left, top); - } - break; - case modeType.view: - $(window).scrollLeft(lastInfo.view.scroll.left); - $(window).scrollTop(lastInfo.view.scroll.top); - break; - case modeType.both: - var left = lastInfo.edit.scroll.left; - var top = lastInfo.edit.scroll.top; - editor.scrollIntoView(); - editor.scrollTo(left, top); - ui.area.view.scrollLeft(lastInfo.view.scroll.left); - ui.area.view.scrollTop(lastInfo.view.scroll.top); - break; - } + } + window.personalInfo['cursor'] = null + socket.emit('cursor blur') +}) - lastInfo.needRestore = false; +function saveInfo () { + var scrollbarStyle = editor.getOption('scrollbarStyle') + var left = $(window).scrollLeft() + var top = $(window).scrollTop() + switch (window.currentMode) { + case modeType.edit: + if (scrollbarStyle === 'native') { + window.lastInfo.edit.scroll.left = left + window.lastInfo.edit.scroll.top = top + } else { + window.lastInfo.edit.scroll = editor.getScrollInfo() + } + break + case modeType.view: + window.lastInfo.view.scroll.left = left + window.lastInfo.view.scroll.top = top + break + case modeType.both: + window.lastInfo.edit.scroll = editor.getScrollInfo() + window.lastInfo.view.scroll.left = ui.area.view.scrollLeft() + window.lastInfo.view.scroll.top = ui.area.view.scrollTop() + break + } + window.lastInfo.edit.cursor = editor.getCursor() + window.lastInfo.edit.selections = editor.listSelections() + window.lastInfo.needRestore = true +} + +function restoreInfo () { + var scrollbarStyle = editor.getOption('scrollbarStyle') + if (window.lastInfo.needRestore) { + var line = window.lastInfo.edit.cursor.line + var ch = window.lastInfo.edit.cursor.ch + editor.setCursor(line, ch) + editor.setSelections(window.lastInfo.edit.selections) + switch (window.currentMode) { + case modeType.edit: + if (scrollbarStyle === 'native') { + $(window).scrollLeft(window.lastInfo.edit.scroll.left) + $(window).scrollTop(window.lastInfo.edit.scroll.top) + } else { + let left = window.lastInfo.edit.scroll.left + let top = window.lastInfo.edit.scroll.top + editor.scrollIntoView() + editor.scrollTo(left, top) + } + break + case modeType.view: + $(window).scrollLeft(window.lastInfo.view.scroll.left) + $(window).scrollTop(window.lastInfo.view.scroll.top) + break + case modeType.both: + let left = window.lastInfo.edit.scroll.left + let top = window.lastInfo.edit.scroll.top + editor.scrollIntoView() + editor.scrollTo(left, top) + ui.area.view.scrollLeft(window.lastInfo.view.scroll.left) + ui.area.view.scrollTop(window.lastInfo.view.scroll.top) + break } + + window.lastInfo.needRestore = false + } } -//view actions -function refreshView() { - ui.area.markdown.html(''); - isDirty = true; - updateViewInner(); +// view actions +function refreshView () { + ui.area.markdown.html('') + window.isDirty = true + updateViewInner() } var updateView = _.debounce(function () { - editor.operation(updateViewInner); -}, updateViewDebounce); - -var lastResult = null; -var postUpdateEvent = null; - -function updateViewInner() { - if (currentMode == modeType.edit || !isDirty) return; - 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 = { - separator: '^(\r\n?|\n)---(\r\n?|\n)$', - verticalSeparator: '^(\r\n?|\n)----(\r\n?|\n)$' - }; - var slides = RevealMarkdown.slidify(editor.getValue(), slideOptions); - ui.area.markdown.html(slides); - RevealMarkdown.initialize(); + editor.operation(updateViewInner) +}, updateViewDebounce) + +var lastResult = null +var postUpdateEvent = null + +function updateViewInner () { + if (window.currentMode === modeType.edit || !window.isDirty) return + 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 = { + separator: '^(\r\n?|\n)---(\r\n?|\n)$', + verticalSeparator: '^(\r\n?|\n)----(\r\n?|\n)$' + } + var slides = window.RevealMarkdown.slidify(editor.getValue(), slideOptions) + ui.area.markdown.html(slides) + window.RevealMarkdown.initialize() // prevent XSS - ui.area.markdown.html(preventXSS(ui.area.markdown.html())); - ui.area.markdown.addClass('slides'); - syncscroll = false; - checkSyncToggle(); - } else { - if (lastMeta.type && lastMeta.type === 'slide') { - refreshView(); - ui.area.markdown.removeClass('slides'); - syncscroll = true; - checkSyncToggle(); - } + ui.area.markdown.html(preventXSS(ui.area.markdown.html())) + ui.area.markdown.addClass('slides') + window.syncscroll = false + checkSyncToggle() + } else { + if (lastMeta.type && lastMeta.type === 'slide') { + refreshView() + ui.area.markdown.removeClass('slides') + window.syncscroll = true + checkSyncToggle() + } // only render again when meta changed - if (JSON.stringify(md.meta) != JSON.stringify(lastMeta)) { - parseMeta(md, ui.area.codemirror, ui.area.markdown, $('#ui-toc'), $('#ui-toc-affix')); - rendered = md.render(value); - } + if (JSON.stringify(md.meta) !== JSON.stringify(lastMeta)) { + parseMeta(md, ui.area.codemirror, ui.area.markdown, $('#ui-toc'), $('#ui-toc-affix')) + rendered = md.render(value) + } // prevent XSS - rendered = preventXSS(rendered); - var result = postProcess(rendered).children().toArray(); - partialUpdate(result, lastResult, ui.area.markdown.children().toArray()); - if (result && lastResult && result.length != lastResult.length) - updateDataAttrs(result, ui.area.markdown.children().toArray()); - lastResult = $(result).clone(); - } - finishView(ui.area.markdown); - autoLinkify(ui.area.markdown); - deduplicatedHeaderId(ui.area.markdown); - renderTOC(ui.area.markdown); - generateToc('ui-toc'); - generateToc('ui-toc-affix'); - generateScrollspy(); - updateScrollspy(); - smoothHashScroll(); - isDirty = false; - clearMap(); - //buildMap(); - updateTitleReminder(); - if (postUpdateEvent && typeof postUpdateEvent === 'function') - postUpdateEvent(); -} - -var updateHistoryDebounce = 600; + rendered = preventXSS(rendered) + var result = postProcess(rendered).children().toArray() + partialUpdate(result, lastResult, ui.area.markdown.children().toArray()) + if (result && lastResult && result.length !== lastResult.length) { updateDataAttrs(result, ui.area.markdown.children().toArray()) } + lastResult = $(result).clone() + } + finishView(ui.area.markdown) + autoLinkify(ui.area.markdown) + deduplicatedHeaderId(ui.area.markdown) + renderTOC(ui.area.markdown) + generateToc('ui-toc') + generateToc('ui-toc-affix') + generateScrollspy() + updateScrollspy() + smoothHashScroll() + window.isDirty = false + clearMap() + // buildMap(); + updateTitleReminder() + if (postUpdateEvent && typeof postUpdateEvent === 'function') { postUpdateEvent() } +} + +var updateHistoryDebounce = 600 var updateHistory = _.debounce(updateHistoryInner, updateHistoryDebounce) -function updateHistoryInner() { - writeHistory(renderFilename(ui.area.markdown), renderTags(ui.area.markdown)); -} - -function updateDataAttrs(src, des) { - //sync data attr startline and endline - for (var i = 0; i < src.length; i++) { - copyAttribute(src[i], des[i], 'data-startline'); - copyAttribute(src[i], des[i], 'data-endline'); - } -} - -function partialUpdate(src, tar, des) { - if (!src || src.length == 0 || !tar || tar.length == 0 || !des || des.length == 0) { - ui.area.markdown.html(src); - return; - } - if (src.length == tar.length) { //same length - for (var i = 0; i < src.length; i++) { - copyAttribute(src[i], des[i], 'data-startline'); - copyAttribute(src[i], des[i], 'data-endline'); - var rawSrc = cloneAndRemoveDataAttr(src[i]); - var rawTar = cloneAndRemoveDataAttr(tar[i]); - if (rawSrc.outerHTML != rawTar.outerHTML) { - //console.log(rawSrc); - //console.log(rawTar); - $(des[i]).replaceWith(src[i]); - } - } - } else { //diff length - var start = 0; - var end = 0; - //find diff start position - for (var i = 0; i < tar.length; i++) { - //copyAttribute(src[i], des[i], 'data-startline'); - //copyAttribute(src[i], des[i], 'data-endline'); - var rawSrc = cloneAndRemoveDataAttr(src[i]); - var rawTar = cloneAndRemoveDataAttr(tar[i]); - if (!rawSrc || !rawTar || rawSrc.outerHTML != rawTar.outerHTML) { - start = i; - break; - } - } - //find diff end position - var srcEnd = 0; - var tarEnd = 0; - for (var i = 0; i < src.length; i++) { - //copyAttribute(src[i], des[i], 'data-startline'); - //copyAttribute(src[i], des[i], 'data-endline'); - var rawSrc = cloneAndRemoveDataAttr(src[i]); - var rawTar = cloneAndRemoveDataAttr(tar[i]); - if (!rawSrc || !rawTar || rawSrc.outerHTML != rawTar.outerHTML) { - start = i; - break; - } - } - //tar end - for (var i = 1; i <= tar.length + 1; i++) { - var srcLength = src.length; - var tarLength = tar.length; - //copyAttribute(src[srcLength - i], des[srcLength - i], 'data-startline'); - //copyAttribute(src[srcLength - i], des[srcLength - i], 'data-endline'); - var rawSrc = cloneAndRemoveDataAttr(src[srcLength - i]); - var rawTar = cloneAndRemoveDataAttr(tar[tarLength - i]); - if (!rawSrc || !rawTar || rawSrc.outerHTML != rawTar.outerHTML) { - tarEnd = tar.length - i; - break; - } - } - //src end - for (var i = 1; i <= src.length + 1; i++) { - var srcLength = src.length; - var tarLength = tar.length; - //copyAttribute(src[srcLength - i], des[srcLength - i], 'data-startline'); - //copyAttribute(src[srcLength - i], des[srcLength - i], 'data-endline'); - var rawSrc = cloneAndRemoveDataAttr(src[srcLength - i]); - var rawTar = cloneAndRemoveDataAttr(tar[tarLength - i]); - if (!rawSrc || !rawTar || rawSrc.outerHTML != rawTar.outerHTML) { - srcEnd = src.length - i; - break; - } - } - //check if tar end overlap tar start - var overlap = 0; - for (var i = start; i >= 0; i--) { - var rawTarStart = cloneAndRemoveDataAttr(tar[i - 1]); - var rawTarEnd = cloneAndRemoveDataAttr(tar[tarEnd + 1 + start - i]); - if (rawTarStart && rawTarEnd && rawTarStart.outerHTML == rawTarEnd.outerHTML) - overlap++; - else - break; - } - if (debug) - console.log('overlap:' + overlap); - //show diff content - if (debug) { - console.log('start:' + start); - console.log('tarEnd:' + tarEnd); - console.log('srcEnd:' + srcEnd); - } - tarEnd += overlap; - srcEnd += overlap; - var repeatAdd = (start - srcEnd) < (start - tarEnd); - var repeatDiff = Math.abs(srcEnd - tarEnd) - 1; - //push new elements - var newElements = []; - if (srcEnd >= start) { - for (var j = start; j <= srcEnd; j++) { - if (!src[j]) continue; - newElements.push(src[j].outerHTML); - } - } else if (repeatAdd) { - for (var j = srcEnd - repeatDiff; j <= srcEnd; j++) { - if (!des[j]) continue; - newElements.push(des[j].outerHTML); - } - } - //push remove elements - var removeElements = []; - if (tarEnd >= start) { - for (var j = start; j <= tarEnd; j++) { - if (!des[j]) continue; - removeElements.push(des[j]); - } - } else if (!repeatAdd) { - for (var j = start; j <= start + repeatDiff; j++) { - if (!des[j]) continue; - removeElements.push(des[j]); - } - } - //add elements - if (debug) { - console.log('ADD ELEMENTS'); - console.log(newElements.join('\n')); - } - if (des[start]) - $(newElements.join('')).insertBefore(des[start]); - else - $(newElements.join('')).insertAfter(des[start - 1]); - //remove elements - if (debug) - console.log('REMOVE ELEMENTS'); - for (var j = 0; j < removeElements.length; j++) { - if (debug) { - console.log(removeElements[j].outerHTML); - } - if (removeElements[j]) - $(removeElements[j]).remove(); - } - } -} - -function cloneAndRemoveDataAttr(el) { - if (!el) return; - var rawEl = $(el).clone(); - rawEl.removeAttr('data-startline data-endline'); - rawEl.find('[data-startline]').removeAttr('data-startline data-endline'); - return rawEl[0]; -} - -function copyAttribute(src, des, attr) { - if (src && src.getAttribute(attr) && des) - des.setAttribute(attr, src.getAttribute(attr)); +function updateHistoryInner () { + writeHistory(renderFilename(ui.area.markdown), renderTags(ui.area.markdown)) +} + +function updateDataAttrs (src, des) { + // sync data attr startline and endline + for (var i = 0; i < src.length; i++) { + copyAttribute(src[i], des[i], 'data-startline') + copyAttribute(src[i], des[i], 'data-endline') + } +} + +function partialUpdate (src, tar, des) { + if (!src || src.length === 0 || !tar || tar.length === 0 || !des || des.length === 0) { + ui.area.markdown.html(src) + return + } + if (src.length === tar.length) { // same length + for (let i = 0; i < src.length; i++) { + copyAttribute(src[i], des[i], 'data-startline') + copyAttribute(src[i], des[i], 'data-endline') + var rawSrc = cloneAndRemoveDataAttr(src[i]) + var rawTar = cloneAndRemoveDataAttr(tar[i]) + if (rawSrc.outerHTML !== rawTar.outerHTML) { + // console.log(rawSrc); + // console.log(rawTar); + $(des[i]).replaceWith(src[i]) + } + } + } else { // diff length + var start = 0 + // find diff start position + for (let i = 0; i < tar.length; i++) { + // copyAttribute(src[i], des[i], 'data-startline'); + // copyAttribute(src[i], des[i], 'data-endline'); + let rawSrc = cloneAndRemoveDataAttr(src[i]) + let rawTar = cloneAndRemoveDataAttr(tar[i]) + if (!rawSrc || !rawTar || rawSrc.outerHTML !== rawTar.outerHTML) { + start = i + break + } + } + // find diff end position + var srcEnd = 0 + var tarEnd = 0 + for (let i = 0; i < src.length; i++) { + // copyAttribute(src[i], des[i], 'data-startline'); + // copyAttribute(src[i], des[i], 'data-endline'); + let rawSrc = cloneAndRemoveDataAttr(src[i]) + let rawTar = cloneAndRemoveDataAttr(tar[i]) + if (!rawSrc || !rawTar || rawSrc.outerHTML !== rawTar.outerHTML) { + start = i + break + } + } + // tar end + for (let i = 1; i <= tar.length + 1; i++) { + let srcLength = src.length + let tarLength = tar.length + // copyAttribute(src[srcLength - i], des[srcLength - i], 'data-startline'); + // copyAttribute(src[srcLength - i], des[srcLength - i], 'data-endline'); + let rawSrc = cloneAndRemoveDataAttr(src[srcLength - i]) + let rawTar = cloneAndRemoveDataAttr(tar[tarLength - i]) + if (!rawSrc || !rawTar || rawSrc.outerHTML !== rawTar.outerHTML) { + tarEnd = tar.length - i + break + } + } + // src end + for (let i = 1; i <= src.length + 1; i++) { + let srcLength = src.length + let tarLength = tar.length + // copyAttribute(src[srcLength - i], des[srcLength - i], 'data-startline'); + // copyAttribute(src[srcLength - i], des[srcLength - i], 'data-endline'); + let rawSrc = cloneAndRemoveDataAttr(src[srcLength - i]) + let rawTar = cloneAndRemoveDataAttr(tar[tarLength - i]) + if (!rawSrc || !rawTar || rawSrc.outerHTML !== rawTar.outerHTML) { + srcEnd = src.length - i + break + } + } + // check if tar end overlap tar start + var overlap = 0 + for (var i = start; i >= 0; i--) { + var rawTarStart = cloneAndRemoveDataAttr(tar[i - 1]) + var rawTarEnd = cloneAndRemoveDataAttr(tar[tarEnd + 1 + start - i]) + if (rawTarStart && rawTarEnd && rawTarStart.outerHTML === rawTarEnd.outerHTML) { overlap++ } else { break } + } + if (debug) { console.log('overlap:' + overlap) } + // show diff content + if (debug) { + console.log('start:' + start) + console.log('tarEnd:' + tarEnd) + console.log('srcEnd:' + srcEnd) + } + tarEnd += overlap + srcEnd += overlap + var repeatAdd = (start - srcEnd) < (start - tarEnd) + var repeatDiff = Math.abs(srcEnd - tarEnd) - 1 + // push new elements + var newElements = [] + if (srcEnd >= start) { + for (let j = start; j <= srcEnd; j++) { + if (!src[j]) continue + newElements.push(src[j].outerHTML) + } + } else if (repeatAdd) { + for (let j = srcEnd - repeatDiff; j <= srcEnd; j++) { + if (!des[j]) continue + newElements.push(des[j].outerHTML) + } + } + // push remove elements + var removeElements = [] + if (tarEnd >= start) { + for (let j = start; j <= tarEnd; j++) { + if (!des[j]) continue + removeElements.push(des[j]) + } + } else if (!repeatAdd) { + for (let j = start; j <= start + repeatDiff; j++) { + if (!des[j]) continue + removeElements.push(des[j]) + } + } + // add elements + if (debug) { + console.log('ADD ELEMENTS') + console.log(newElements.join('\n')) + } + if (des[start]) { $(newElements.join('')).insertBefore(des[start]) } else { $(newElements.join('')).insertAfter(des[start - 1]) } + // remove elements + if (debug) { console.log('REMOVE ELEMENTS') } + for (let j = 0; j < removeElements.length; j++) { + if (debug) { + console.log(removeElements[j].outerHTML) + } + if (removeElements[j]) { $(removeElements[j]).remove() } + } + } +} + +function cloneAndRemoveDataAttr (el) { + if (!el) return + var rawEl = $(el).clone() + rawEl.removeAttr('data-startline data-endline') + rawEl.find('[data-startline]').removeAttr('data-startline data-endline') + return rawEl[0] +} + +function copyAttribute (src, des, attr) { + if (src && src.getAttribute(attr) && des) { des.setAttribute(attr, src.getAttribute(attr)) } } if ($('.cursor-menu').length <= 0) { - $("<div class='cursor-menu'>").insertAfter('.CodeMirror-cursors'); + $("<div class='cursor-menu'>").insertAfter('.CodeMirror-cursors') } -function reverseSortCursorMenu(dropdown) { - var items = dropdown.find('.textcomplete-item'); - items.sort(function (a, b) { - return $(b).attr('data-index') - $(a).attr('data-index'); - }); - return items; +function reverseSortCursorMenu (dropdown) { + var items = dropdown.find('.textcomplete-item') + items.sort(function (a, b) { + return $(b).attr('data-index') - $(a).attr('data-index') + }) + return items } -var checkCursorMenu = _.throttle(checkCursorMenuInner, cursorMenuThrottle); +var checkCursorMenu = _.throttle(checkCursorMenuInner, cursorMenuThrottle) -function checkCursorMenuInner() { +function checkCursorMenuInner () { // get element - var dropdown = $('.cursor-menu > .dropdown-menu'); + var dropdown = $('.cursor-menu > .dropdown-menu') // return if not exists - if (dropdown.length <= 0) return; + if (dropdown.length <= 0) return // set margin - var menuRightMargin = 10; - var menuBottomMargin = 4; + var menuRightMargin = 10 + var menuBottomMargin = 4 // use sizer to get the real doc size (won't count status bar and gutters) - var docWidth = ui.area.codemirrorSizer.width(); - var docHeight = ui.area.codemirrorSizer.height(); + var docWidth = ui.area.codemirrorSizer.width() // get editor size (status bar not count in) - var editorWidth = ui.area.codemirror.width(); - var editorHeight = ui.area.codemirror.height(); + var editorHeight = ui.area.codemirror.height() // get element size - var width = dropdown.outerWidth(); - var height = dropdown.outerHeight(); + var width = dropdown.outerWidth() + var height = dropdown.outerHeight() // get cursor - var cursor = editor.getCursor(); + var cursor = editor.getCursor() // set element cursor data - if (!dropdown.hasClass('CodeMirror-other-cursor')) - dropdown.addClass('CodeMirror-other-cursor'); - dropdown.attr('data-line', cursor.line); - dropdown.attr('data-ch', cursor.ch); + if (!dropdown.hasClass('CodeMirror-other-cursor')) { dropdown.addClass('CodeMirror-other-cursor') } + dropdown.attr('data-line', cursor.line) + dropdown.attr('data-ch', cursor.ch) // get coord position - var coord = editor.charCoords({ - line: cursor.line, - ch: cursor.ch - }, 'windows'); - var left = coord.left; - var top = coord.top; + var coord = editor.charCoords({ + line: cursor.line, + ch: cursor.ch + }, 'windows') + var left = coord.left + var top = coord.top // get doc top offset (to workaround with viewport) - var docTopOffset = ui.area.codemirrorSizerInner.position().top; + var docTopOffset = ui.area.codemirrorSizerInner.position().top // set offset - var offsetLeft = 0; - var offsetTop = defaultTextHeight; + var offsetLeft = 0 + var offsetTop = defaultTextHeight // set up side down - window.upSideDown = false; - var lastUpSideDown = upSideDown = false; + window.upSideDown = false + var lastUpSideDown = window.upSideDown = false // only do when have width and height - if (width > 0 && height > 0) { + if (width > 0 && height > 0) { // make element right bound not larger than doc width - if (left + width + offsetLeft + menuRightMargin > docWidth) - offsetLeft = -(left + width - docWidth + menuRightMargin); + if (left + width + offsetLeft + menuRightMargin > docWidth) { offsetLeft = -(left + width - docWidth + menuRightMargin) } // flip y when element bottom bound larger than doc height // and element top position is larger than element height - if (top + docTopOffset + height + offsetTop + menuBottomMargin > Math.max(editor.doc.height, editorHeight) && top + docTopOffset > height + menuBottomMargin) { - offsetTop = -(height + menuBottomMargin); + if (top + docTopOffset + height + offsetTop + menuBottomMargin > Math.max(editor.doc.height, editorHeight) && top + docTopOffset > height + menuBottomMargin) { + offsetTop = -(height + menuBottomMargin) // reverse sort menu because upSideDown - dropdown.html(reverseSortCursorMenu(dropdown)); - upSideDown = true; - } - var textCompleteDropdown = $(editor.getInputField()).data('textComplete').dropdown; - lastUpSideDown = textCompleteDropdown.upSideDown; - textCompleteDropdown.upSideDown = upSideDown; + dropdown.html(reverseSortCursorMenu(dropdown)) + window.upSideDown = true } + var textCompleteDropdown = $(editor.getInputField()).data('textComplete').dropdown + lastUpSideDown = textCompleteDropdown.upSideDown + textCompleteDropdown.upSideDown = window.upSideDown + } // make menu scroll top only if upSideDown changed - if (upSideDown !== lastUpSideDown) - dropdown.scrollTop(dropdown[0].scrollHeight); + if (window.upSideDown !== lastUpSideDown) { dropdown.scrollTop(dropdown[0].scrollHeight) } // set element offset data - dropdown.attr('data-offset-left', offsetLeft); - dropdown.attr('data-offset-top', offsetTop); + dropdown.attr('data-offset-left', offsetLeft) + dropdown.attr('data-offset-top', offsetTop) // set position - dropdown[0].style.left = left + offsetLeft + 'px'; - dropdown[0].style.top = top + offsetTop + 'px'; + dropdown[0].style.left = left + offsetLeft + 'px' + dropdown[0].style.top = top + offsetTop + 'px' } -function checkInIndentCode() { +function checkInIndentCode () { // if line starts with tab or four spaces is a code block - var line = editor.getLine(editor.getCursor().line); - var isIndentCode = ((line.substr(0, 4) === ' ') || (line.substr(0, 1) === '\t')); - return isIndentCode; -} - -var isInCode = false; - -function checkInCode() { - isInCode = checkAbove(matchInCode) || checkInIndentCode(); -} - -function checkAbove(method) { - var cursor = editor.getCursor(); - var text = []; - for (var i = 0; i < cursor.line; i++) //contain current line - text.push(editor.getLine(i)); - text = text.join('\n') + '\n' + editor.getLine(cursor.line).slice(0, cursor.ch); - //console.log(text); - return method(text); -} - -function checkBelow(method) { - var cursor = editor.getCursor(); - var count = editor.lineCount(); - var text = []; - for (var i = cursor.line + 1; i < count; i++) //contain current line - text.push(editor.getLine(i)); - text = editor.getLine(cursor.line).slice(cursor.ch) + '\n' + text.join('\n'); - //console.log(text); - return method(text); -} - -function matchInCode(text) { - var match; - match = text.match(/`{3,}/g); + var line = editor.getLine(editor.getCursor().line) + var isIndentCode = ((line.substr(0, 4) === ' ') || (line.substr(0, 1) === '\t')) + return isIndentCode +} + +var isInCode = false + +function checkInCode () { + isInCode = checkAbove(matchInCode) || checkInIndentCode() +} + +function checkAbove (method) { + var cursor = editor.getCursor() + var text = [] + for (var i = 0; i < cursor.line; i++) { // contain current line + text.push(editor.getLine(i)) + } + text = text.join('\n') + '\n' + editor.getLine(cursor.line).slice(0, cursor.ch) + // console.log(text); + return method(text) +} + +function checkBelow (method) { + var cursor = editor.getCursor() + var count = editor.lineCount() + var text = [] + for (var i = cursor.line + 1; i < count; i++) { // contain current line + text.push(editor.getLine(i)) + } + text = editor.getLine(cursor.line).slice(cursor.ch) + '\n' + text.join('\n') + // console.log(text); + return method(text) +} + +function matchInCode (text) { + var match + match = text.match(/`{3,}/g) + if (match && match.length % 2) { + return true + } else { + match = text.match(/`/g) if (match && match.length % 2) { - return true; + return true } else { - match = text.match(/`/g); - if (match && match.length % 2) { - return true; - } else { - return false; - } + return false } + } } -var isInContainer = false; -var isInContainerSyntax = false; +var isInContainer = false +var isInContainerSyntax = false -function checkInContainer() { - isInContainer = checkAbove(matchInContainer) && !checkInIndentCode(); +function checkInContainer () { + isInContainer = checkAbove(matchInContainer) && !checkInIndentCode() } -function checkInContainerSyntax() { +function checkInContainerSyntax () { // if line starts with :::, it's in container syntax - var line = editor.getLine(editor.getCursor().line); - isInContainerSyntax = (line.substr(0, 3) === ':::'); + var line = editor.getLine(editor.getCursor().line) + isInContainerSyntax = (line.substr(0, 3) === ':::') } -function matchInContainer(text) { - var match; - match = text.match(/:{3,}/g); - if (match && match.length % 2) { - return true; - } else { - return false; - } +function matchInContainer (text) { + var match + match = text.match(/:{3,}/g) + if (match && match.length % 2) { + return true + } else { + return false + } } $(editor.getInputField()) .textcomplete([ - { // emoji strategy - match: /(^|\n|\s)\B:([\-+\w]*)$/, - search: function (term, callback) { - var line = editor.getLine(editor.getCursor().line); - term = line.match(this.match)[2]; - var list = []; - $.map(emojify.emojiNames, function (emoji) { - if (emoji.indexOf(term) === 0) //match at first character - list.push(emoji); - }); - $.map(emojify.emojiNames, function (emoji) { - if (emoji.indexOf(term) !== -1) //match inside the word - list.push(emoji); - }); - callback(list); - }, - template: function (value) { - return '<img class="emoji" src="' + serverurl + '/build/emojify.js/dist/images/basic/' + value + '.png"></img> ' + value; - }, - replace: function (value) { - return '$1:' + value + ': '; - }, - index: 1, - context: function (text) { - checkInCode(); - checkInContainer(); - checkInContainerSyntax(); - return !isInCode && !isInContainerSyntax; + { // emoji strategy + match: /(^|\n|\s)\B:([-+\w]*)$/, + search: function (term, callback) { + var line = editor.getLine(editor.getCursor().line) + term = line.match(this.match)[2] + var list = [] + $.map(window.emojify.emojiNames, function (emoji) { + if (emoji.indexOf(term) === 0) { // match at first character + list.push(emoji) } - }, - { // Code block language strategy - langs: supportCodeModes, - charts: supportCharts, - match: /(^|\n)```(\w+)$/, - search: function (term, callback) { - var line = editor.getLine(editor.getCursor().line); - term = line.match(this.match)[2]; - var list = []; - $.map(this.langs, function (lang) { - if (lang.indexOf(term) === 0 && lang !== term) - list.push(lang); - }); - $.map(this.charts, function (chart) { - if (chart.indexOf(term) === 0 && chart !== term) - list.push(chart); - }); - callback(list); - }, - replace: function (lang) { - var ending = ''; - if (!checkBelow(matchInCode)) { - ending = '\n\n```'; - } - if (this.langs.indexOf(lang) !== -1) - return '$1```' + lang + '=' + ending; - else if (this.charts.indexOf(lang) !== -1) - return '$1```' + lang + ending; - }, - done: function () { - var cursor = editor.getCursor(); - var text = []; - text.push(editor.getLine(cursor.line - 1)); - text.push(editor.getLine(cursor.line)); - text = text.join('\n'); - //console.log(text); - if (text == '\n```') - editor.doc.cm.execCommand("goLineUp"); - }, - context: function (text) { - return isInCode; + }) + $.map(window.emojify.emojiNames, function (emoji) { + if (emoji.indexOf(term) !== -1) { // match inside the word + list.push(emoji) } + }) + callback(list) }, - { // Container strategy - containers: supportContainers, - match: /(^|\n):::(\s*)(\w*)$/, - search: function (term, callback) { - var line = editor.getLine(editor.getCursor().line); - term = line.match(this.match)[3].trim(); - var list = []; - $.map(this.containers, function (container) { - if (container.indexOf(term) === 0 && container !== term) - list.push(container); - }); - callback(list); - }, - replace: function (lang) { - var ending = ''; - if (!checkBelow(matchInContainer)) { - ending = '\n\n:::'; - } - if (this.containers.indexOf(lang) !== -1) - return '$1:::$2' + lang + ending; - }, - done: function () { - var cursor = editor.getCursor(); - var text = []; - text.push(editor.getLine(cursor.line - 1)); - text.push(editor.getLine(cursor.line)); - text = text.join('\n'); - //console.log(text); - if (text == '\n:::') - editor.doc.cm.execCommand("goLineUp"); - }, - context: function (text) { - return !isInCode && isInContainer; - } + template: function (value) { + return '<img class="emoji" src="' + serverurl + '/build/emojify.js/dist/images/basic/' + value + '.png"></img> ' + value }, - { //header - match: /(?:^|\n)(\s{0,3})(#{1,6}\w*)$/, - search: function (term, callback) { - callback($.map(supportHeaders, function (header) { - return header.search.indexOf(term) === 0 ? header.text : null; - })); - }, - replace: function (value) { - return '$1' + value; - }, - context: function (text) { - return !isInCode; - } + replace: function (value) { + return '$1:' + value + ': ' }, - { //extra tags for blockquote - match: /(?:^|\n|\s)(\>.*|\s|)((\^|)\[(\^|)\](\[\]|\(\)|\:|)\s*\w*)$/, - search: function (term, callback) { - var line = editor.getLine(editor.getCursor().line); - var quote = line.match(this.match)[1].trim(); - var list = []; - if (quote.indexOf('>') == 0) { - $.map(supportExtraTags, function (extratag) { - if (extratag.search.indexOf(term) === 0) - list.push(extratag.command()); - }); - } - $.map(supportReferrals, function (referral) { - if (referral.search.indexOf(term) === 0) - list.push(referral.text); - }) - callback(list); - }, - replace: function (value) { - return '$1' + value; - }, - context: function (text) { - return !isInCode; - } + index: 1, + context: function (text) { + checkInCode() + checkInContainer() + checkInContainerSyntax() + return !isInCode && !isInContainerSyntax + } + }, + { // Code block language strategy + langs: supportCodeModes, + charts: supportCharts, + match: /(^|\n)```(\w+)$/, + search: function (term, callback) { + var line = editor.getLine(editor.getCursor().line) + term = line.match(this.match)[2] + var list = [] + $.map(this.langs, function (lang) { + if (lang.indexOf(term) === 0 && lang !== term) { list.push(lang) } + }) + $.map(this.charts, function (chart) { + if (chart.indexOf(term) === 0 && chart !== term) { list.push(chart) } + }) + callback(list) }, - { //extra tags for list - match: /(^[>\s]*[\-\+\*]\s(?:\[[x ]\]|.*))(\[\])(\w*)$/, - search: function (term, callback) { - var list = []; - $.map(supportExtraTags, function (extratag) { - if (extratag.search.indexOf(term) === 0) - list.push(extratag.command()); - }); - $.map(supportReferrals, function (referral) { - if (referral.search.indexOf(term) === 0) - list.push(referral.text); - }) - callback(list); - }, - replace: function (value) { - return '$1' + value; - }, - context: function (text) { - return !isInCode; - } + replace: function (lang) { + var ending = '' + if (!checkBelow(matchInCode)) { + ending = '\n\n```' + } + if (this.langs.indexOf(lang) !== -1) { return '$1```' + lang + '=' + ending } else if (this.charts.indexOf(lang) !== -1) { return '$1```' + lang + ending } }, - { //referral - match: /(^\s*|\n|\s{2})((\[\]|\[\]\[\]|\[\]\(\)|\!|\!\[\]|\!\[\]\[\]|\!\[\]\(\))\s*\w*)$/, - search: function (term, callback) { - callback($.map(supportReferrals, function (referral) { - return referral.search.indexOf(term) === 0 ? referral.text : null; - })); - }, - replace: function (value) { - return '$1' + value; - }, - context: function (text) { - return !isInCode; - } + done: function () { + var cursor = editor.getCursor() + var text = [] + text.push(editor.getLine(cursor.line - 1)) + text.push(editor.getLine(cursor.line)) + text = text.join('\n') + // console.log(text); + if (text === '\n```') { editor.doc.cm.execCommand('goLineUp') } }, - { //externals - match: /(^|\n|\s)\{\}(\w*)$/, - search: function (term, callback) { - callback($.map(supportExternals, function (external) { - return external.search.indexOf(term) === 0 ? external.text : null; - })); - }, - replace: function (value) { - return '$1' + value; - }, - context: function (text) { - return !isInCode; - } - } - ], { - appendTo: $('.cursor-menu') - }) - .on({ - 'textComplete:beforeSearch': function (e) { - //NA + context: function (text) { + return isInCode + } + }, + { // Container strategy + containers: supportContainers, + match: /(^|\n):::(\s*)(\w*)$/, + search: function (term, callback) { + var line = editor.getLine(editor.getCursor().line) + term = line.match(this.match)[3].trim() + var list = [] + $.map(this.containers, function (container) { + if (container.indexOf(term) === 0 && container !== term) { list.push(container) } + }) + callback(list) }, - 'textComplete:afterSearch': function (e) { - checkCursorMenu(); + replace: function (lang) { + var ending = '' + if (!checkBelow(matchInContainer)) { + ending = '\n\n:::' + } + if (this.containers.indexOf(lang) !== -1) { return '$1:::$2' + lang + ending } }, - 'textComplete:select': function (e, value, strategy) { - //NA + done: function () { + var cursor = editor.getCursor() + var text = [] + text.push(editor.getLine(cursor.line - 1)) + text.push(editor.getLine(cursor.line)) + text = text.join('\n') + // console.log(text); + if (text === '\n:::') { editor.doc.cm.execCommand('goLineUp') } }, - 'textComplete:show': function (e) { - $(this).data('autocompleting', true); - editor.setOption("extraKeys", { - "Up": function () { - return false; - }, - "Right": function () { - editor.doc.cm.execCommand("goCharRight"); - }, - "Down": function () { - return false; - }, - "Left": function () { - editor.doc.cm.execCommand("goCharLeft"); - }, - "Enter": function () { - return false; - }, - "Backspace": function () { - editor.doc.cm.execCommand("delCharBefore"); - } - }); + context: function (text) { + return !isInCode && isInContainer + } + }, + { // header + match: /(?:^|\n)(\s{0,3})(#{1,6}\w*)$/, + search: function (term, callback) { + callback($.map(supportHeaders, function (header) { + return header.search.indexOf(term) === 0 ? header.text : null + })) + }, + replace: function (value) { + return '$1' + value + }, + context: function (text) { + return !isInCode + } + }, + { // extra tags for blockquote + match: /(?:^|\n|\s)(>.*|\s|)((\^|)\[(\^|)\](\[\]|\(\)|:|)\s*\w*)$/, + search: function (term, callback) { + var line = editor.getLine(editor.getCursor().line) + var quote = line.match(this.match)[1].trim() + var list = [] + if (quote.indexOf('>') === 0) { + $.map(supportExtraTags, function (extratag) { + if (extratag.search.indexOf(term) === 0) { list.push(extratag.command()) } + }) + } + $.map(supportReferrals, function (referral) { + if (referral.search.indexOf(term) === 0) { list.push(referral.text) } + }) + callback(list) + }, + replace: function (value) { + return '$1' + value + }, + context: function (text) { + return !isInCode + } + }, + { // extra tags for list + match: /(^[>\s]*[-+*]\s(?:\[[x ]\]|.*))(\[\])(\w*)$/, + search: function (term, callback) { + var list = [] + $.map(supportExtraTags, function (extratag) { + if (extratag.search.indexOf(term) === 0) { list.push(extratag.command()) } + }) + $.map(supportReferrals, function (referral) { + if (referral.search.indexOf(term) === 0) { list.push(referral.text) } + }) + callback(list) }, - 'textComplete:hide': function (e) { - $(this).data('autocompleting', false); - editor.setOption("extraKeys", defaultExtraKeys); + replace: function (value) { + return '$1' + value + }, + context: function (text) { + return !isInCode + } + }, + { // referral + match: /(^\s*|\n|\s{2})((\[\]|\[\]\[\]|\[\]\(\)|!|!\[\]|!\[\]\[\]|!\[\]\(\))\s*\w*)$/, + search: function (term, callback) { + callback($.map(supportReferrals, function (referral) { + return referral.search.indexOf(term) === 0 ? referral.text : null + })) + }, + replace: function (value) { + return '$1' + value + }, + context: function (text) { + return !isInCode + } + }, + { // externals + match: /(^|\n|\s)\{\}(\w*)$/, + search: function (term, callback) { + callback($.map(supportExternals, function (external) { + return external.search.indexOf(term) === 0 ? external.text : null + })) + }, + replace: function (value) { + return '$1' + value + }, + context: function (text) { + return !isInCode } - }); + } + ], { + appendTo: $('.cursor-menu') + }) + .on({ + 'textComplete:beforeSearch': function (e) { + // NA + }, + 'textComplete:afterSearch': function (e) { + checkCursorMenu() + }, + 'textComplete:select': function (e, value, strategy) { + // NA + }, + 'textComplete:show': function (e) { + $(this).data('autocompleting', true) + editor.setOption('extraKeys', { + 'Up': function () { + return false + }, + 'Right': function () { + editor.doc.cm.execCommand('goCharRight') + }, + 'Down': function () { + return false + }, + 'Left': function () { + editor.doc.cm.execCommand('goCharLeft') + }, + 'Enter': function () { + return false + }, + 'Backspace': function () { + editor.doc.cm.execCommand('delCharBefore') + } + }) + }, + 'textComplete:hide': function (e) { + $(this).data('autocompleting', false) + editor.setOption('extraKeys', defaultExtraKeys) + } + }) diff --git a/public/js/lib/common/login.js b/public/js/lib/common/login.js index 58fa55c6..18cd377d 100644 --- a/public/js/lib/common/login.js +++ b/public/js/lib/common/login.js @@ -1,89 +1,92 @@ -import { serverurl } from '../config'; +/* eslint-env browser, jquery */ +/* global Cookies */ -let checkAuth = false; -let profile = null; -let lastLoginState = getLoginState(); -let lastUserId = getUserId(); -var loginStateChangeEvent = null; +import { serverurl } from '../config' -export function setloginStateChangeEvent(func) { - loginStateChangeEvent = func; +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 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 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 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 getLoginState () { + const state = Cookies.get('loginstate') + return state === 'true' || state === true } -export function getUserId() { - return Cookies.get('userid'); +export function getUserId () { + return Cookies.get('userid') } -export function clearLoginState() { - Cookies.remove('loginstate'); +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`) +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); - } + if (data && data.status === 'ok') { + profile = data + yesCallback(profile) + setLoginState(true, data.id) + } else { + noCallback() + setLoginState(false) + } }) .fail(() => { - noCallback(); + noCallback() }) .always(() => { - checkAuth = true; - }); - } else if (cookieLoginState) { - yesCallback(profile); - } else { - noCallback(); - } + checkAuth = true + }) + } else if (cookieLoginState) { + yesCallback(profile) + } else { + noCallback() + } } export default { - checkAuth, - profile, - lastLoginState, - lastUserId, - loginStateChangeEvent -}; + checkAuth, + profile, + lastLoginState, + lastUserId, + loginStateChangeEvent +} diff --git a/public/js/lib/config/index.js b/public/js/lib/config/index.js index 2b73679f..1ea7a7ab 100644 --- a/public/js/lib/config/index.js +++ b/public/js/lib/config/index.js @@ -1,19 +1,19 @@ -import configJson from '../../../../config.json'; // root path json config +import configJson from '../../../../config.json' // root path json config -const config = 'production' === process.env.NODE_ENV ? configJson.production : configJson.development; +const config = process.env.NODE_ENV === 'production' ? 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 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 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 port = window.location.port +export const serverurl = `${window.location.protocol}//${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'; +export const version = '0.5.0' diff --git a/public/js/locale.js b/public/js/locale.js index e6d11cd2..2a2c1814 100644 --- a/public/js/locale.js +++ b/public/js/locale.js @@ -1,26 +1,28 @@ -var lang = "en"; -var userLang = navigator.language || navigator.userLanguage; -var userLangCode = userLang.split('-')[0]; -var userCountryCode = userLang.split('-')[1]; -var locale = $('.ui-locale'); -var supportLangs = []; -$(".ui-locale option").each(function() { - supportLangs.push($(this).val()); -}); +/* eslint-env browser, jquery */ +/* global Cookies */ + +var lang = 'en' +var userLang = navigator.language || navigator.userLanguage +var userLangCode = userLang.split('-')[0] +var locale = $('.ui-locale') +var supportLangs = [] +$('.ui-locale option').each(function () { + supportLangs.push($(this).val()) +}) if (Cookies.get('locale')) { - lang = Cookies.get('locale'); + lang = Cookies.get('locale') } else if (supportLangs.indexOf(userLang) !== -1) { - lang = supportLangs[supportLangs.indexOf(userLang)]; + lang = supportLangs[supportLangs.indexOf(userLang)] } else if (supportLangs.indexOf(userLangCode) !== -1) { - lang = supportLangs[supportLangs.indexOf(userLangCode)]; + lang = supportLangs[supportLangs.indexOf(userLangCode)] } -locale.val(lang); -$('select.ui-locale option[value="' + lang + '"]').attr('selected','selected'); +locale.val(lang) +$('select.ui-locale option[value="' + lang + '"]').attr('selected', 'selected') -locale.change(function() { - Cookies.set('locale', $(this).val(), { - expires: 365 - }); - window.location.reload(); -}); +locale.change(function () { + Cookies.set('locale', $(this).val(), { + expires: 365 + }) + window.location.reload() +}) diff --git a/public/js/pretty.js b/public/js/pretty.js index 18d0dc0d..718941a8 100644 --- a/public/js/pretty.js +++ b/public/js/pretty.js @@ -1,8 +1,11 @@ -require('../css/extra.css'); -require('../css/slide-preview.css'); -require('../css/site.css'); +/* eslint-env browser, jquery */ +/* global refreshView */ -require('highlight.js/styles/github-gist.css'); +require('../css/extra.css') +require('../css/slide-preview.css') +require('../css/site.css') + +require('highlight.js/styles/github-gist.css') import { autoLinkify, @@ -16,126 +19,126 @@ import { scrollToHash, smoothHashScroll, updateLastChange -} from './extra'; +} from './extra' -import { preventXSS } from './render'; +import { preventXSS } from './render' -const markdown = $("#doc.markdown-body"); -const text = markdown.text(); -const lastMeta = md.meta; -md.meta = {}; -delete md.metaError; -let rendered = md.render(text); +const markdown = $('#doc.markdown-body') +const text = markdown.text() +const lastMeta = md.meta +md.meta = {} +delete md.metaError +let rendered = md.render(text) if (md.meta.type && md.meta.type === 'slide') { - const slideOptions = { - separator: '^(\r\n?|\n)---(\r\n?|\n)$', - verticalSeparator: '^(\r\n?|\n)----(\r\n?|\n)$' - }; - const slides = RevealMarkdown.slidify(text, slideOptions); - markdown.html(slides); - RevealMarkdown.initialize(); + const slideOptions = { + separator: '^(\r\n?|\n)---(\r\n?|\n)$', + verticalSeparator: '^(\r\n?|\n)----(\r\n?|\n)$' + } + const slides = window.RevealMarkdown.slidify(text, slideOptions) + markdown.html(slides) + window.RevealMarkdown.initialize() // prevent XSS - markdown.html(preventXSS(markdown.html())); - markdown.addClass('slides'); + markdown.html(preventXSS(markdown.html())) + markdown.addClass('slides') } else { - if (lastMeta.type && lastMeta.type === 'slide') { - refreshView(); - markdown.removeClass('slides'); - } + if (lastMeta.type && lastMeta.type === 'slide') { + refreshView() + markdown.removeClass('slides') + } // only render again when meta changed - if (JSON.stringify(md.meta) != JSON.stringify(lastMeta)) { - parseMeta(md, null, markdown, $('#ui-toc'), $('#ui-toc-affix')); - rendered = md.render(text); - } + if (JSON.stringify(md.meta) !== JSON.stringify(lastMeta)) { + parseMeta(md, null, markdown, $('#ui-toc'), $('#ui-toc-affix')) + rendered = md.render(text) + } // prevent XSS - rendered = preventXSS(rendered); - const result = postProcess(rendered); - markdown.html(result.html()); + rendered = preventXSS(rendered) + const result = postProcess(rendered) + markdown.html(result.html()) } -$(document.body).show(); +$(document.body).show() -finishView(markdown); -autoLinkify(markdown); -deduplicatedHeaderId(markdown); -renderTOC(markdown); -generateToc('ui-toc'); -generateToc('ui-toc-affix'); -smoothHashScroll(); -createtime = lastchangeui.time.attr('data-createtime'); -lastchangetime = lastchangeui.time.attr('data-updatetime'); -updateLastChange(); +finishView(markdown) +autoLinkify(markdown) +deduplicatedHeaderId(markdown) +renderTOC(markdown) +generateToc('ui-toc') +generateToc('ui-toc-affix') +smoothHashScroll() +window.createtime = window.lastchangeui.time.attr('data-createtime') +window.lastchangetime = window.lastchangeui.time.attr('data-updatetime') +updateLastChange() -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 +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(e => { - e.stopPropagation(); -}); + e.stopPropagation() +}) -let enoughForAffixToc = true; +let enoughForAffixToc = true -function generateScrollspy() { - $(document.body).scrollspy({ - target: '' - }); - $(document.body).scrollspy('refresh'); - if (enoughForAffixToc) { - toc.hide(); - tocAffix.show(); - } else { - tocAffix.hide(); - toc.show(); - } - $(document.body).scroll(); +function generateScrollspy () { + $(document.body).scrollspy({ + target: '' + }) + $(document.body).scrollspy('refresh') + if (enoughForAffixToc) { + toc.hide() + tocAffix.show() + } else { + tocAffix.hide() + toc.show() + } + $(document.body).scroll() } -function windowResize() { - //toc right - 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 - let newbool; - const rightMargin = (markdown.parent().outerWidth() - markdown.outerWidth()) / 2; - //for ipad or wider device - if (rightMargin >= 133) { - newbool = true; - const affixLeftMargin = (tocAffix.outerWidth() - tocAffix.width()) / 2; - const left = markdown.offset().left + markdown.outerWidth() - affixLeftMargin; - tocAffix.css('left', `${left}px`); - } else { - newbool = false; - } - if (newbool != enoughForAffixToc) { - enoughForAffixToc = newbool; - generateScrollspy(); - } +function windowResize () { + // toc right + 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 + let newbool + const rightMargin = (markdown.parent().outerWidth() - markdown.outerWidth()) / 2 + // for ipad or wider device + if (rightMargin >= 133) { + newbool = true + const affixLeftMargin = (tocAffix.outerWidth() - tocAffix.width()) / 2 + const left = markdown.offset().left + markdown.outerWidth() - affixLeftMargin + tocAffix.css('left', `${left}px`) + } else { + newbool = false + } + if (newbool !== enoughForAffixToc) { + enoughForAffixToc = newbool + generateScrollspy() + } } $(window).resize(() => { - windowResize(); -}); + windowResize() +}) $(document).ready(() => { - windowResize(); - generateScrollspy(); - setTimeout(scrollToHash, 0); - //tooltip - $('[data-toggle="tooltip"]').tooltip(); -}); + windowResize() + generateScrollspy() + setTimeout(scrollToHash, 0) + // tooltip + $('[data-toggle="tooltip"]').tooltip() +}) -export function scrollToTop() { - $('body, html').stop(true, true).animate({ - scrollTop: 0 - }, 100, "linear"); +export function scrollToTop () { + $('body, html').stop(true, true).animate({ + scrollTop: 0 + }, 100, 'linear') } -export function scrollToBottom() { - $('body, html').stop(true, true).animate({ - scrollTop: $(document.body)[0].scrollHeight - }, 100, "linear"); +export function scrollToBottom () { + $('body, html').stop(true, true).animate({ + scrollTop: $(document.body)[0].scrollHeight + }, 100, 'linear') } -window.scrollToTop = scrollToTop; -window.scrollToBottom = scrollToBottom; +window.scrollToTop = scrollToTop +window.scrollToBottom = scrollToBottom diff --git a/public/js/render.js b/public/js/render.js index 5d6d0aa2..61663a4b 100644 --- a/public/js/render.js +++ b/public/js/render.js @@ -1,62 +1,64 @@ +/* eslint-env browser, jquery */ +/* global filterXSS */ // allow some attributes -var whiteListAttr = ['id', 'class', 'style']; -window.whiteListAttr = whiteListAttr; +var whiteListAttr = ['id', 'class', 'style'] +window.whiteListAttr = whiteListAttr // allow link starts with '.', '/' and custom protocol with '://' -var linkRegex = /^([\w|-]+:\/\/)|^([\.|\/])+/; +var linkRegex = /^([\w|-]+:\/\/)|^([.|/])+/ // allow data uri, from https://gist.github.com/bgrins/6194623 -var dataUriRegex = /^\s*data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@\/?%\s]*)\s*$/i; +var dataUriRegex = /^\s*data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@/?%\s]*)\s*$/i // custom white list -var whiteList = filterXSS.whiteList; +var whiteList = filterXSS.whiteList // allow ol specify start number -whiteList['ol'] = ['start']; +whiteList['ol'] = ['start'] // allow li specify value number -whiteList['li'] = ['value']; +whiteList['li'] = ['value'] // allow style tag -whiteList['style'] = []; +whiteList['style'] = [] // allow kbd tag -whiteList['kbd'] = []; +whiteList['kbd'] = [] // allow ifram tag with some safe attributes -whiteList['iframe'] = ['allowfullscreen', 'name', 'referrerpolicy', 'sandbox', 'src', 'srcdoc', 'width', 'height']; +whiteList['iframe'] = ['allowfullscreen', 'name', 'referrerpolicy', 'sandbox', 'src', 'srcdoc', 'width', 'height'] // allow summary tag -whiteList['summary'] = []; +whiteList['summary'] = [] var filterXSSOptions = { - allowCommentTag: true, - whiteList: whiteList, - escapeHtml: function (html) { + allowCommentTag: true, + whiteList: whiteList, + escapeHtml: function (html) { // allow html comment in multiple lines - return html.replace(/<(.*?)>/g, '<$1>'); - }, - onIgnoreTag: function (tag, html, options) { + return html.replace(/<(.*?)>/g, '<$1>') + }, + onIgnoreTag: function (tag, html, options) { // allow comment tag - if (tag == "!--") { + if (tag === '!--') { // do not filter its attributes - return html; - } - }, - onTagAttr: function (tag, name, value, isWhiteAttr) { + return html + } + }, + onTagAttr: function (tag, name, value, isWhiteAttr) { // allow href and src that match linkRegex - if (isWhiteAttr && (name === 'href' || name === 'src') && linkRegex.test(value)) { - return name + '="' + filterXSS.escapeAttrValue(value) + '"'; - } + if (isWhiteAttr && (name === 'href' || name === 'src') && linkRegex.test(value)) { + return name + '="' + filterXSS.escapeAttrValue(value) + '"' + } // allow data uri in img src - if (isWhiteAttr && (tag == "img" && name === 'src') && dataUriRegex.test(value)) { - return name + '="' + filterXSS.escapeAttrValue(value) + '"'; - } - }, - onIgnoreTagAttr: function (tag, name, value, isWhiteAttr) { + if (isWhiteAttr && (tag === 'img' && name === 'src') && dataUriRegex.test(value)) { + return name + '="' + filterXSS.escapeAttrValue(value) + '"' + } + }, + onIgnoreTagAttr: function (tag, name, value, isWhiteAttr) { // allow attr start with 'data-' or in the whiteListAttr - if (name.substr(0, 5) === 'data-' || whiteListAttr.indexOf(name) !== -1) { + if (name.substr(0, 5) === 'data-' || window.whiteListAttr.indexOf(name) !== -1) { // escape its value using built-in escapeAttrValue function - return name + '="' + filterXSS.escapeAttrValue(value) + '"'; - } + return name + '="' + filterXSS.escapeAttrValue(value) + '"' } -}; + } +} -function preventXSS(html) { - return filterXSS(html, filterXSSOptions); +function preventXSS (html) { + return filterXSS(html, filterXSSOptions) } -window.preventXSS = preventXSS; +window.preventXSS = preventXSS module.exports = { preventXSS: preventXSS diff --git a/public/js/reveal-markdown.js b/public/js/reveal-markdown.js index 3c3e1f5b..eca148d8 100755 --- a/public/js/reveal-markdown.js +++ b/public/js/reveal-markdown.js @@ -1,396 +1,355 @@ +/* eslint-env browser, jquery */ + +import { preventXSS } from './render' +import { md } from './extra' + /** * The reveal.js markdown plugin. Handles parsing of * markdown inside of presentations as well as loading * of external markdown documents. */ -(function( root, factory ) { - if( typeof exports === 'object' ) { - module.exports = factory(); - } - else { - // Browser globals (root is window) - root.RevealMarkdown = factory(); - root.RevealMarkdown.initialize(); - } -}( this, function() { - - var DEFAULT_SLIDE_SEPARATOR = '^\r?\n---\r?\n$', - DEFAULT_NOTES_SEPARATOR = 'note:', - DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR = '\\\.element\\\s*?(.+?)$', - DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR = '\\\.slide:\\\s*?(\\\S.+?)$'; - - var SCRIPT_END_PLACEHOLDER = '__SCRIPT_END__'; - - - /** - * Retrieves the markdown contents of a slide section - * element. Normalizes leading tabs/whitespace. - */ - function getMarkdownFromSlide( section ) { - - var template = section.querySelector( 'script' ); - - // strip leading whitespace so it isn't evaluated as code - var text = ( template || section ).textContent; - - // restore script end tags - text = text.replace( new RegExp( SCRIPT_END_PLACEHOLDER, 'g' ), '</script>' ); - - var leadingWs = text.match( /^\n?(\s*)/ )[1].length, - leadingTabs = text.match( /^\n?(\t*)/ )[1].length; - - if( leadingTabs > 0 ) { - text = text.replace( new RegExp('\\n?\\t{' + leadingTabs + '}','g'), '\n' ); - } - else if( leadingWs > 1 ) { - text = text.replace( new RegExp('\\n? {' + leadingWs + '}', 'g'), '\n' ); - } - - return text; - - } - - /** - * Given a markdown slide section element, this will - * return all arguments that aren't related to markdown - * parsing. Used to forward any other user-defined arguments - * to the output markdown slide. - */ - function getForwardedAttributes( section ) { - - var attributes = section.attributes; - var result = []; - - for( var i = 0, len = attributes.length; i < len; i++ ) { - var name = attributes[i].name, - value = attributes[i].value; - - // disregard attributes that are used for markdown loading/parsing - if( /data\-(markdown|separator|vertical|notes)/gi.test( name ) ) continue; - - if( value ) { - result.push( name + '="' + value + '"' ); - } - else { - result.push( name ); - } - } - - return result.join( ' ' ); - - } - - /** - * Inspects the given options and fills out default - * values for what's not defined. - */ - function getSlidifyOptions( options ) { - - options = options || {}; - options.separator = options.separator || DEFAULT_SLIDE_SEPARATOR; - options.notesSeparator = options.notesSeparator || DEFAULT_NOTES_SEPARATOR; - options.attributes = options.attributes || ''; - - return options; - - } - - /** - * Helper function for constructing a markdown slide. - */ - function createMarkdownSlide( content, options ) { - - options = getSlidifyOptions( options ); - - var notesMatch = content.split( new RegExp( options.notesSeparator, 'mgi' ) ); - - if( notesMatch.length === 2 ) { - content = notesMatch[0] + '<aside class="notes" data-markdown>' + notesMatch[1].trim() + '</aside>'; - } - - // prevent script end tags in the content from interfering - // with parsing - content = content.replace( /<\/script>/g, SCRIPT_END_PLACEHOLDER ); - - return '<script type="text/template">' + content + '</script>'; - - } - - /** - * Parses a data string into multiple slides based - * on the passed in separator arguments. - */ - function slidify( markdown, options ) { - - options = getSlidifyOptions( options ); - - var separatorRegex = new RegExp( options.separator + ( options.verticalSeparator ? '|' + options.verticalSeparator : '' ), 'mg' ), - horizontalSeparatorRegex = new RegExp( options.separator ); - - var matches, - lastIndex = 0, - isHorizontal, - wasHorizontal = true, - content, - sectionStack = []; - - // iterate until all blocks between separators are stacked up - while( matches = separatorRegex.exec( markdown ) ) { - notes = null; - - // determine direction (horizontal by default) - isHorizontal = horizontalSeparatorRegex.test( matches[0] ); - - if( !isHorizontal && wasHorizontal ) { - // create vertical stack - sectionStack.push( [] ); - } - - // pluck slide content from markdown input - content = markdown.substring( lastIndex, matches.index ); - - if( isHorizontal && wasHorizontal ) { - // add to horizontal stack - sectionStack.push( content ); - } - else { - // add to vertical stack - sectionStack[sectionStack.length-1].push( content ); - } - - lastIndex = separatorRegex.lastIndex; - wasHorizontal = isHorizontal; - } - - // add the remaining slide - ( wasHorizontal ? sectionStack : sectionStack[sectionStack.length-1] ).push( markdown.substring( lastIndex ) ); - - var markdownSections = ''; - - // flatten the hierarchical stack, and insert <section data-markdown> tags - for( var i = 0, len = sectionStack.length; i < len; i++ ) { - // vertical - if( sectionStack[i] instanceof Array ) { - markdownSections += '<section '+ options.attributes +'>'; - - sectionStack[i].forEach( function( child ) { - markdownSections += '<section data-markdown>' + createMarkdownSlide( child, options ) + '</section>'; - } ); - - markdownSections += '</section>'; - } - else { - markdownSections += '<section '+ options.attributes +' data-markdown>' + createMarkdownSlide( sectionStack[i], options ) + '</section>'; - } - } - - return markdownSections; - - } - - /** - * Parses any current data-markdown slides, splits - * multi-slide markdown into separate sections and - * handles loading of external markdown. - */ - function processSlides() { - - var sections = document.querySelectorAll( '[data-markdown]'), - section; - - for( var i = 0, len = sections.length; i < len; i++ ) { - - section = sections[i]; - - if( section.getAttribute( 'data-markdown' ).length ) { - - var xhr = new XMLHttpRequest(), - url = section.getAttribute( 'data-markdown' ); - - datacharset = section.getAttribute( 'data-charset' ); - - // see https://developer.mozilla.org/en-US/docs/Web/API/element.getAttribute#Notes - if( datacharset != null && datacharset != '' ) { - xhr.overrideMimeType( 'text/html; charset=' + datacharset ); - } - - xhr.onreadystatechange = function() { - if( xhr.readyState === 4 ) { - // file protocol yields status code 0 (useful for local debug, mobile applications etc.) - if ( ( xhr.status >= 200 && xhr.status < 300 ) || xhr.status === 0 ) { - - section.outerHTML = slidify( xhr.responseText, { - separator: section.getAttribute( 'data-separator' ), - verticalSeparator: section.getAttribute( 'data-separator-vertical' ), - notesSeparator: section.getAttribute( 'data-separator-notes' ), - attributes: getForwardedAttributes( section ) - }); - - } - else { - - section.outerHTML = '<section data-state="alert">' + - 'ERROR: The attempt to fetch ' + url + ' failed with HTTP status ' + xhr.status + '.' + - 'Check your browser\'s JavaScript console for more details.' + - '<p>Remember that you need to serve the presentation HTML from a HTTP server.</p>' + - '</section>'; - - } - } - }; - - xhr.open( 'GET', url, false ); - - try { - xhr.send(); - } - catch ( e ) { - alert( 'Failed to get the Markdown file ' + url + '. Make sure that the presentation and the file are served by a HTTP server and the file can be found there. ' + e ); - } - - } - else if( section.getAttribute( 'data-separator' ) || section.getAttribute( 'data-separator-vertical' ) || section.getAttribute( 'data-separator-notes' ) ) { - - section.outerHTML = slidify( getMarkdownFromSlide( section ), { - separator: section.getAttribute( 'data-separator' ), - verticalSeparator: section.getAttribute( 'data-separator-vertical' ), - notesSeparator: section.getAttribute( 'data-separator-notes' ), - attributes: getForwardedAttributes( section ) - }); - - } - else { - section.innerHTML = createMarkdownSlide( getMarkdownFromSlide( section ) ); - } - } - - } - - /** - * Check if a node value has the attributes pattern. - * If yes, extract it and add that value as one or several attributes - * the the terget element. - * - * You need Cache Killer on Chrome to see the effect on any FOM transformation - * directly on refresh (F5) - * http://stackoverflow.com/questions/5690269/disabling-chrome-cache-for-website-development/7000899#answer-11786277 - */ - function addAttributeInElement( node, elementTarget, separator ) { - - var mardownClassesInElementsRegex = new RegExp( separator, 'mg' ); - var mardownClassRegex = new RegExp( "([^\"= ]+?)=\"([^\"=]+?)\"", 'mg' ); - var nodeValue = node.nodeValue; - if( matches = mardownClassesInElementsRegex.exec( nodeValue ) ) { - - var classes = matches[1]; - nodeValue = nodeValue.substring( 0, matches.index ) + nodeValue.substring( mardownClassesInElementsRegex.lastIndex ); - node.nodeValue = nodeValue; - while( matchesClass = mardownClassRegex.exec( classes ) ) { - var name = matchesClass[1]; - var value = matchesClass[2]; - if (name.substr(0, 5) === 'data-' || whiteListAttr.indexOf(name) !== -1) - elementTarget.setAttribute( name, filterXSS.escapeAttrValue(value) ); - } - return true; - } - return false; - } - - /** - * Add attributes to the parent element of a text node, - * or the element of an attribute node. - */ - function addAttributes( section, element, previousElement, separatorElementAttributes, separatorSectionAttributes ) { - - if ( element != null && element.childNodes != undefined && element.childNodes.length > 0 ) { - previousParentElement = element; - for( var i = 0; i < element.childNodes.length; i++ ) { - childElement = element.childNodes[i]; - if ( i > 0 ) { - j = i - 1; - while ( j >= 0 ) { - aPreviousChildElement = element.childNodes[j]; - if ( typeof aPreviousChildElement.setAttribute == 'function' && aPreviousChildElement.tagName != "BR" ) { - previousParentElement = aPreviousChildElement; - break; - } - j = j - 1; - } - } - parentSection = section; - if( childElement.nodeName == "section" ) { - parentSection = childElement ; - previousParentElement = childElement ; - } - if ( typeof childElement.setAttribute == 'function' || childElement.nodeType == Node.COMMENT_NODE ) { - addAttributes( parentSection, childElement, previousParentElement, separatorElementAttributes, separatorSectionAttributes ); - } - } - } - - if ( element.nodeType == Node.COMMENT_NODE ) { - if ( addAttributeInElement( element, previousElement, separatorElementAttributes ) == false ) { - addAttributeInElement( element, section, separatorSectionAttributes ); - } - } - } - - /** - * Converts any current data-markdown slides in the - * DOM to HTML. - */ - function convertSlides() { - - var sections = document.querySelectorAll( '[data-markdown]'); - - for( var i = 0, len = sections.length; i < len; i++ ) { - - var section = sections[i]; - - // Only parse the same slide once - if( !section.getAttribute( 'data-markdown-parsed' ) ) { - - section.setAttribute( 'data-markdown-parsed', true ) - - var notes = section.querySelector( 'aside.notes' ); - var markdown = getMarkdownFromSlide( section ); - - var rendered = md.render(markdown); - rendered = preventXSS(rendered); - var result = postProcess(rendered); - section.innerHTML = result[0].outerHTML; - addAttributes( section, section, null, section.getAttribute( 'data-element-attributes' ) || - section.parentNode.getAttribute( 'data-element-attributes' ) || - DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR, - section.getAttribute( 'data-attributes' ) || - section.parentNode.getAttribute( 'data-attributes' ) || - DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR); - - // If there were notes, we need to re-add them after - // having overwritten the section's HTML - if( notes ) { - section.appendChild( notes ); - } - - } - - } - - } - - // API - return { - - initialize: function() { - processSlides(); - convertSlides(); - }, - - // TODO: Do these belong in the API? - processSlides: processSlides, - convertSlides: convertSlides, - slidify: slidify - - }; - -})); +(function (root, factory) { + if (typeof exports === 'object') { + module.exports = factory() + } else { + // Browser globals (root is window) + root.RevealMarkdown = factory() + root.RevealMarkdown.initialize() + } +}(this, function () { + var DEFAULT_SLIDE_SEPARATOR = '^\r?\n---\r?\n$' + var DEFAULT_NOTES_SEPARATOR = 'note:' + var DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR = '\\.element\\s*?(.+?)$' + var DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR = '\\.slide:\\s*?(\\S.+?)$' + + var SCRIPT_END_PLACEHOLDER = '__SCRIPT_END__' + + /** + * Retrieves the markdown contents of a slide section + * element. Normalizes leading tabs/whitespace. + */ + function getMarkdownFromSlide (section) { + var template = section.querySelector('script') + + // strip leading whitespace so it isn't evaluated as code + var text = (template || section).textContent + + // restore script end tags + text = text.replace(new RegExp(SCRIPT_END_PLACEHOLDER, 'g'), '</script>') + + var leadingWs = text.match(/^\n?(\s*)/)[1].length + var leadingTabs = text.match(/^\n?(\t*)/)[1].length + + if (leadingTabs > 0) { + text = text.replace(new RegExp('\\n?\\t{' + leadingTabs + '}', 'g'), '\n') + } else if (leadingWs > 1) { + text = text.replace(new RegExp('\\n? {' + leadingWs + '}', 'g'), '\n') + } + + return text + } + + /** + * Given a markdown slide section element, this will + * return all arguments that aren't related to markdown + * parsing. Used to forward any other user-defined arguments + * to the output markdown slide. + */ + function getForwardedAttributes (section) { + var attributes = section.attributes + var result = [] + + for (var i = 0, len = attributes.length; i < len; i++) { + var name = attributes[i].name + var value = attributes[i].value + + // disregard attributes that are used for markdown loading/parsing + if (/data-(markdown|separator|vertical|notes)/gi.test(name)) continue + + if (value) { + result.push(name + '="' + value + '"') + } else { + result.push(name) + } + } + + return result.join(' ') + } + + /** + * Inspects the given options and fills out default + * values for what's not defined. + */ + function getSlidifyOptions (options) { + options = options || {} + options.separator = options.separator || DEFAULT_SLIDE_SEPARATOR + options.notesSeparator = options.notesSeparator || DEFAULT_NOTES_SEPARATOR + options.attributes = options.attributes || '' + + return options + } + + /** + * Helper function for constructing a markdown slide. + */ + function createMarkdownSlide (content, options) { + options = getSlidifyOptions(options) + + var notesMatch = content.split(new RegExp(options.notesSeparator, 'mgi')) + + if (notesMatch.length === 2) { + content = notesMatch[0] + '<aside class="notes" data-markdown>' + notesMatch[1].trim() + '</aside>' + } + + // prevent script end tags in the content from interfering + // with parsing + content = content.replace(/<\/script>/g, SCRIPT_END_PLACEHOLDER) + + return '<script type="text/template">' + content + '</script>' + } + + /** + * Parses a data string into multiple slides based + * on the passed in separator arguments. + */ + function slidify (markdown, options) { + options = getSlidifyOptions(options) + + var separatorRegex = new RegExp(options.separator + (options.verticalSeparator ? '|' + options.verticalSeparator : ''), 'mg') + var horizontalSeparatorRegex = new RegExp(options.separator) + + var matches + var lastIndex = 0 + var isHorizontal + var wasHorizontal = true + var content + var sectionStack = [] + + // iterate until all blocks between separators are stacked up + while ((matches = separatorRegex.exec(markdown)) !== null) { + // determine direction (horizontal by default) + isHorizontal = horizontalSeparatorRegex.test(matches[0]) + + if (!isHorizontal && wasHorizontal) { + // create vertical stack + sectionStack.push([]) + } + + // pluck slide content from markdown input + content = markdown.substring(lastIndex, matches.index) + + if (isHorizontal && wasHorizontal) { + // add to horizontal stack + sectionStack.push(content) + } else { + // add to vertical stack + sectionStack[sectionStack.length - 1].push(content) + } + + lastIndex = separatorRegex.lastIndex + wasHorizontal = isHorizontal + } + + // add the remaining slide + (wasHorizontal ? sectionStack : sectionStack[sectionStack.length - 1]).push(markdown.substring(lastIndex)) + + var markdownSections = '' + + // flatten the hierarchical stack, and insert <section data-markdown> tags + for (var i = 0, len = sectionStack.length; i < len; i++) { + // vertical + if (sectionStack[i] instanceof Array) { + markdownSections += '<section ' + options.attributes + '>' + + sectionStack[i].forEach(function (child) { + markdownSections += '<section data-markdown>' + createMarkdownSlide(child, options) + '</section>' + }) + + markdownSections += '</section>' + } else { + markdownSections += '<section ' + options.attributes + ' data-markdown>' + createMarkdownSlide(sectionStack[i], options) + '</section>' + } + } + + return markdownSections + } + + /** + * Parses any current data-markdown slides, splits + * multi-slide markdown into separate sections and + * handles loading of external markdown. + */ + function processSlides () { + var sections = document.querySelectorAll('[data-markdown]') + var section + + for (var i = 0, len = sections.length; i < len; i++) { + section = sections[i] + + if (section.getAttribute('data-markdown').length) { + var xhr = new XMLHttpRequest() + var url = section.getAttribute('data-markdown') + + var datacharset = section.getAttribute('data-charset') + + // see https://developer.mozilla.org/en-US/docs/Web/API/element.getAttribute#Notes + if (datacharset !== null && datacharset !== '') { + xhr.overrideMimeType('text/html; charset=' + datacharset) + } + + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + // file protocol yields status code 0 (useful for local debug, mobile applications etc.) + if ((xhr.status >= 200 && xhr.status < 300) || xhr.status === 0) { + section.outerHTML = slidify(xhr.responseText, { + separator: section.getAttribute('data-separator'), + verticalSeparator: section.getAttribute('data-separator-vertical'), + notesSeparator: section.getAttribute('data-separator-notes'), + attributes: getForwardedAttributes(section) + }) + } else { + section.outerHTML = '<section data-state="alert">' + + 'ERROR: The attempt to fetch ' + url + ' failed with HTTP status ' + xhr.status + '.' + + 'Check your browser\'s JavaScript console for more details.' + + '<p>Remember that you need to serve the presentation HTML from a HTTP server.</p>' + + '</section>' + } + } + } + + xhr.open('GET', url, false) + + try { + xhr.send() + } catch (e) { + alert('Failed to get the Markdown file ' + url + '. Make sure that the presentation and the file are served by a HTTP server and the file can be found there. ' + e) + } + } else if (section.getAttribute('data-separator') || section.getAttribute('data-separator-vertical') || section.getAttribute('data-separator-notes')) { + section.outerHTML = slidify(getMarkdownFromSlide(section), { + separator: section.getAttribute('data-separator'), + verticalSeparator: section.getAttribute('data-separator-vertical'), + notesSeparator: section.getAttribute('data-separator-notes'), + attributes: getForwardedAttributes(section) + }) + } else { + section.innerHTML = createMarkdownSlide(getMarkdownFromSlide(section)) + } + } + } + + /** + * Check if a node value has the attributes pattern. + * If yes, extract it and add that value as one or several attributes + * the the terget element. + * + * You need Cache Killer on Chrome to see the effect on any FOM transformation + * directly on refresh (F5) + * http://stackoverflow.com/questions/5690269/disabling-chrome-cache-for-website-development/7000899#answer-11786277 + */ + function addAttributeInElement (node, elementTarget, separator) { + var mardownClassesInElementsRegex = new RegExp(separator, 'mg') + var mardownClassRegex = new RegExp('([^"= ]+?)="([^"=]+?)"', 'mg') + var nodeValue = node.nodeValue + var matches + var matchesClass + if ((matches = mardownClassesInElementsRegex.exec(nodeValue))) { + var classes = matches[1] + nodeValue = nodeValue.substring(0, matches.index) + nodeValue.substring(mardownClassesInElementsRegex.lastIndex) + node.nodeValue = nodeValue + while ((matchesClass = mardownClassRegex.exec(classes))) { + var name = matchesClass[1] + var value = matchesClass[2] + if (name.substr(0, 5) === 'data-' || window.whiteListAttr.indexOf(name) !== -1) { elementTarget.setAttribute(name, window.filterXSS.escapeAttrValue(value)) } + } + return true + } + return false + } + + /** + * Add attributes to the parent element of a text node, + * or the element of an attribute node. + */ + function addAttributes (section, element, previousElement, separatorElementAttributes, separatorSectionAttributes) { + if (element != null && element.childNodes !== undefined && element.childNodes.length > 0) { + var previousParentElement = element + for (var i = 0; i < element.childNodes.length; i++) { + var childElement = element.childNodes[i] + if (i > 0) { + let j = i - 1 + while (j >= 0) { + var aPreviousChildElement = element.childNodes[j] + if (typeof aPreviousChildElement.setAttribute === 'function' && aPreviousChildElement.tagName !== 'BR') { + previousParentElement = aPreviousChildElement + break + } + j = j - 1 + } + } + var parentSection = section + if (childElement.nodeName === 'section') { + parentSection = childElement + previousParentElement = childElement + } + if (typeof childElement.setAttribute === 'function' || childElement.nodeType === Node.COMMENT_NODE) { + addAttributes(parentSection, childElement, previousParentElement, separatorElementAttributes, separatorSectionAttributes) + } + } + } + + if (element.nodeType === Node.COMMENT_NODE) { + if (addAttributeInElement(element, previousElement, separatorElementAttributes) === false) { + addAttributeInElement(element, section, separatorSectionAttributes) + } + } + } + + /** + * Converts any current data-markdown slides in the + * DOM to HTML. + */ + function convertSlides () { + var sections = document.querySelectorAll('[data-markdown]') + + for (var i = 0, len = sections.length; i < len; i++) { + var section = sections[i] + + // Only parse the same slide once + if (!section.getAttribute('data-markdown-parsed')) { + section.setAttribute('data-markdown-parsed', true) + + var notes = section.querySelector('aside.notes') + var markdown = getMarkdownFromSlide(section) + + var rendered = md.render(markdown) + rendered = preventXSS(rendered) + var result = window.postProcess(rendered) + section.innerHTML = result[0].outerHTML + addAttributes(section, section, null, section.getAttribute('data-element-attributes') || + section.parentNode.getAttribute('data-element-attributes') || + DEFAULT_ELEMENT_ATTRIBUTES_SEPARATOR, + section.getAttribute('data-attributes') || + section.parentNode.getAttribute('data-attributes') || + DEFAULT_SLIDE_ATTRIBUTES_SEPARATOR) + + // If there were notes, we need to re-add them after + // having overwritten the section's HTML + if (notes) { + section.appendChild(notes) + } + } + } + } + + // API + return { + initialize: function () { + processSlides() + convertSlides() + }, + // TODO: Do these belong in the API? + processSlides: processSlides, + convertSlides: convertSlides, + slidify: slidify + } +})) diff --git a/public/js/slide.js b/public/js/slide.js index 63cf64c6..e743bb55 100644 --- a/public/js/slide.js +++ b/public/js/slide.js @@ -1,138 +1,139 @@ -require('../css/extra.css'); -require('../css/site.css'); +/* eslint-env browser, jquery */ +/* global serverurl, Reveal */ -import { md, updateLastChange, finishView } from './extra'; +require('../css/extra.css') +require('../css/site.css') -import { preventXSS } from './render'; +import { md, updateLastChange, finishView } from './extra' -const body = $(".slides").text(); +const body = $('.slides').text() -createtime = lastchangeui.time.attr('data-createtime'); -lastchangetime = lastchangeui.time.attr('data-updatetime'); -updateLastChange(); -const url = window.location.pathname; -$('.ui-edit').attr('href', `${url}/edit`); +window.createtime = window.lastchangeui.time.attr('data-createtime') +window.lastchangetime = window.lastchangeui.time.attr('data-updatetime') +updateLastChange() +const url = window.location.pathname +$('.ui-edit').attr('href', `${url}/edit`) $(document).ready(() => { - //tooltip - $('[data-toggle="tooltip"]').tooltip(); -}); - -function extend() { - const target = {}; - - for (const source of arguments) { - for (const key in source) { - if (source.hasOwnProperty(key)) { - target[key] = source[key]; - } - } + // tooltip + $('[data-toggle="tooltip"]').tooltip() +}) + +function extend () { + const target = {} + + for (const source of arguments) { + for (const key in source) { + if (source.hasOwnProperty(key)) { + target[key] = source[key] + } } + } - return target; + return target } // Optional libraries used to extend on reveal.js const deps = [{ - src: `${serverurl}/build/reveal.js/lib/js/classList.js`, - condition() { - return !document.body.classList; - } + src: `${serverurl}/build/reveal.js/lib/js/classList.js`, + condition () { + return !document.body.classList + } }, { - src: `${serverurl}/js/reveal-markdown.js`, - callback() { - const slideOptions = { - separator: '^(\r\n?|\n)---(\r\n?|\n)$', - verticalSeparator: '^(\r\n?|\n)----(\r\n?|\n)$' - }; - const slides = RevealMarkdown.slidify(body, slideOptions); - $(".slides").html(slides); - RevealMarkdown.initialize(); - $(".slides").show(); + src: `${serverurl}/js/reveal-markdown.js`, + callback () { + const slideOptions = { + separator: '^(\r\n?|\n)---(\r\n?|\n)$', + verticalSeparator: '^(\r\n?|\n)----(\r\n?|\n)$' } + const slides = window.RevealMarkdown.slidify(body, slideOptions) + $('.slides').html(slides) + window.RevealMarkdown.initialize() + $('.slides').show() + } }, { - src: `${serverurl}/build/reveal.js/plugin/notes/notes.js`, - async: true, - condition() { - return !!document.body.classList; - } -}]; + src: `${serverurl}/build/reveal.js/plugin/notes/notes.js`, + async: true, + condition () { + return !!document.body.classList + } +}] // default options to init reveal.js const defaultOptions = { - controls: true, - progress: true, - slideNumber: true, - history: true, - center: true, - transition: 'none', - dependencies: deps -}; + controls: true, + progress: true, + slideNumber: true, + history: true, + center: true, + transition: 'none', + dependencies: deps +} // options from yaml meta -const meta = JSON.parse($("#meta").text()); -var options = meta.slideOptions || {}; +const meta = JSON.parse($('#meta').text()) +var options = meta.slideOptions || {} -const view = $('.reveal'); +const view = $('.reveal') -//text language -if (meta.lang && typeof meta.lang == "string") { - view.attr('lang', meta.lang); +// text language +if (meta.lang && typeof meta.lang === 'string') { + view.attr('lang', meta.lang) } else { - view.removeAttr('lang'); + view.removeAttr('lang') } -//text direction -if (meta.dir && typeof meta.dir == "string" && meta.dir == "rtl") { - options.rtl = true; +// text direction +if (meta.dir && typeof meta.dir === 'string' && meta.dir === 'rtl') { + options.rtl = true } else { - options.rtl = false; + options.rtl = false } -//breaks +// breaks if (typeof meta.breaks === 'boolean' && !meta.breaks) { - md.options.breaks = false; + md.options.breaks = false } else { - md.options.breaks = true; + md.options.breaks = true } // options from URL query string -const queryOptions = Reveal.getQueryHash() || {}; +const queryOptions = Reveal.getQueryHash() || {} -var options = extend(defaultOptions, options, queryOptions); -Reveal.initialize(options); +options = extend(defaultOptions, options, queryOptions) +Reveal.initialize(options) window.viewAjaxCallback = () => { - Reveal.layout(); -}; - -function renderSlide(event) { - if (window.location.search.match( /print-pdf/gi )) { - const slides = $('.slides'); - var title = document.title; - finishView(slides); - document.title = title; - Reveal.layout(); - } else { - const markdown = $(event.currentSlide); - if (!markdown.attr('data-rendered')) { - var title = document.title; - finishView(markdown); - markdown.attr('data-rendered', 'true'); - document.title = title; - Reveal.layout(); - } + Reveal.layout() +} + +function renderSlide (event) { + if (window.location.search.match(/print-pdf/gi)) { + const slides = $('.slides') + let title = document.title + finishView(slides) + document.title = title + Reveal.layout() + } else { + const markdown = $(event.currentSlide) + if (!markdown.attr('data-rendered')) { + let title = document.title + finishView(markdown) + markdown.attr('data-rendered', 'true') + document.title = title + Reveal.layout() } + } } Reveal.addEventListener('ready', event => { - renderSlide(event); - const markdown = $(event.currentSlide); + renderSlide(event) + const markdown = $(event.currentSlide) // force browser redraw - setTimeout(() => { - markdown.hide().show(0); - }, 0); -}); -Reveal.addEventListener('slidechanged', renderSlide); + setTimeout(() => { + markdown.hide().show(0) + }, 0) +}) +Reveal.addEventListener('slidechanged', renderSlide) -const isMacLike = navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i) ? true : false; +const isMacLike = !!navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i) -if (!isMacLike) $('.container').addClass('hidescrollbar'); +if (!isMacLike) $('.container').addClass('hidescrollbar') diff --git a/public/js/syncscroll.js b/public/js/syncscroll.js index c9693176..c227f83f 100644 --- a/public/js/syncscroll.js +++ b/public/js/syncscroll.js @@ -1,365 +1,367 @@ +/* eslint-env browser, jquery */ +/* global _ */ // Inject line numbers for sync scroll. -import markdownitContainer from 'markdown-it-container'; +import markdownitContainer from 'markdown-it-container' -import { md } from './extra'; +import { md } from './extra' -function addPart(tokens, idx) { - if (tokens[idx].map && tokens[idx].level === 0) { - 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); - } +function addPart (tokens, idx) { + if (tokens[idx].map && tokens[idx].level === 0) { + 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) + } } md.renderer.rules.blockquote_open = function (tokens, idx, options, env, self) { - tokens[idx].attrJoin('class', 'raw'); - addPart(tokens, idx); - return self.renderToken(...arguments); -}; + tokens[idx].attrJoin('class', 'raw') + addPart(tokens, idx) + return self.renderToken(...arguments) +} md.renderer.rules.table_open = function (tokens, idx, options, env, self) { - addPart(tokens, idx); - return self.renderToken(...arguments); -}; + addPart(tokens, idx) + return self.renderToken(...arguments) +} md.renderer.rules.bullet_list_open = function (tokens, idx, options, env, self) { - addPart(tokens, idx); - return self.renderToken(...arguments); -}; + addPart(tokens, idx) + 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) { - 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(...arguments); -}; + tokens[idx].attrJoin('class', 'raw') + if (tokens[idx].map) { + 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(...arguments) +} md.renderer.rules.ordered_list_open = function (tokens, idx, options, env, self) { - addPart(tokens, idx); - return self.renderToken(...arguments); -}; + addPart(tokens, idx) + return self.renderToken(...arguments) +} md.renderer.rules.link_open = function (tokens, idx, options, env, self) { - addPart(tokens, idx); - return self.renderToken(...arguments); -}; + addPart(tokens, idx) + return self.renderToken(...arguments) +} md.renderer.rules.paragraph_open = function (tokens, idx, options, env, self) { - addPart(tokens, idx); - return self.renderToken(...arguments); -}; + addPart(tokens, idx) + 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(...arguments); -}; + tokens[idx].attrJoin('class', 'raw') + addPart(tokens, idx) + return self.renderToken(...arguments) +} 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]; - if (/\!$/.test(info)) token.attrJoin('class', 'wrap'); - token.attrJoin('class', options.langPrefix + langName.replace(/\=$|\=\d+$|\=\+$|\!$|\=\!/, '')); - token.attrJoin('class', 'hljs'); - token.attrJoin('class', 'raw'); - } - - if (options.highlight) { - highlighted = options.highlight(token.content, langName) || md.utils.escapeHtml(token.content); - } else { - highlighted = md.utils.escapeHtml(token.content); - } - - if (highlighted.indexOf('<pre') === 0) { - return `${highlighted}\n`; - } - - if (tokens[idx].map && tokens[idx].level === 0) { - 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`; -}; + 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] + if (/!$/.test(info)) token.attrJoin('class', 'wrap') + token.attrJoin('class', options.langPrefix + langName.replace(/=$|=\d+$|=\+$|!$|=!/, '')) + token.attrJoin('class', 'hljs') + token.attrJoin('class', 'raw') + } + + if (options.highlight) { + highlighted = options.highlight(token.content, langName) || md.utils.escapeHtml(token.content) + } else { + highlighted = md.utils.escapeHtml(token.content) + } + + if (highlighted.indexOf('<pre') === 0) { + return `${highlighted}\n` + } + + if (tokens[idx].map && tokens[idx].level === 0) { + 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` +} md.renderer.rules.code_block = (tokens, idx, options, env, self) => { - if (tokens[idx].map && tokens[idx].level === 0) { - 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`; -}; -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()}`); - addPart(tokens, idx); - return self.renderToken(...arguments); + if (tokens[idx].map && tokens[idx].level === 0) { + 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` +} +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()}`) + addPart(tokens, idx) + return self.renderToken(...arguments) } -md.use(markdownitContainer, 'success', { render: renderContainer }); -md.use(markdownitContainer, 'info', { render: renderContainer }); -md.use(markdownitContainer, 'warning', { render: renderContainer }); -md.use(markdownitContainer, 'danger', { render: renderContainer }); +md.use(markdownitContainer, 'success', { render: renderContainer }) +md.use(markdownitContainer, 'info', { render: renderContainer }) +md.use(markdownitContainer, 'warning', { render: renderContainer }) +md.use(markdownitContainer, 'danger', { render: renderContainer }) // FIXME: expose syncscroll to window -window.syncscroll = true; +window.syncscroll = true -window.preventSyncScrollToEdit = false; -window.preventSyncScrollToView = false; +window.preventSyncScrollToEdit = false +window.preventSyncScrollToView = false -const editScrollThrottle = 5; -const viewScrollThrottle = 5; -const buildMapThrottle = 100; +const editScrollThrottle = 5 +const viewScrollThrottle = 5 +const buildMapThrottle = 100 -let viewScrolling = false; -let editScrolling = false; +let viewScrolling = false +let editScrolling = false -let editArea = null; -let viewArea = null; -let markdownArea = null; +let editArea = null +let viewArea = null +let markdownArea = null -export function setupSyncAreas(edit, view, markdown) { - editArea = edit; - viewArea = view; - markdownArea = markdown; - editArea.on('scroll', _.throttle(syncScrollToView, editScrollThrottle)); - viewArea.on('scroll', _.throttle(syncScrollToEdit, viewScrollThrottle)); +export function setupSyncAreas (edit, view, markdown) { + editArea = edit + viewArea = view + markdownArea = markdown + editArea.on('scroll', _.throttle(syncScrollToView, editScrollThrottle)) + viewArea.on('scroll', _.throttle(syncScrollToEdit, viewScrollThrottle)) } -let scrollMap, lineHeightMap, viewTop, viewBottom; +let scrollMap, lineHeightMap, viewTop, viewBottom -export function clearMap() { - scrollMap = null; - lineHeightMap = null; - viewTop = null; - viewBottom = null; +export function clearMap () { + scrollMap = null + lineHeightMap = null + viewTop = null + viewBottom = null } -window.viewAjaxCallback = clearMap; +window.viewAjaxCallback = clearMap -const 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; - let i, offset, nonEmptyList, pos, a, b, _lineHeightMap, linesCount, acc, _scrollMap; - - offset = viewArea.scrollTop() - viewArea.offset().top; - _scrollMap = []; - nonEmptyList = []; - _lineHeightMap = []; - viewTop = 0; - viewBottom = viewArea[0].scrollHeight - viewArea.height(); - - acc = 0; - const lines = editor.getValue().split('\n'); - const lineHeight = editor.defaultTextHeight(); - for (i = 0; i < lines.length; i++) { - const str = lines[i]; - - _lineHeightMap.push(acc); - - if (str.length === 0) { - acc++; - continue; - } - - const h = editor.heightAtLine(i + 1) - editor.heightAtLine(i); - acc += Math.round(h / lineHeight); +function buildMapInner (callback) { + if (!viewArea || !markdownArea) return + let i, offset, nonEmptyList, pos, a, b, _lineHeightMap, linesCount, acc, _scrollMap + + offset = viewArea.scrollTop() - viewArea.offset().top + _scrollMap = [] + nonEmptyList = [] + _lineHeightMap = [] + viewTop = 0 + viewBottom = viewArea[0].scrollHeight - viewArea.height() + + acc = 0 + const lines = window.editor.getValue().split('\n') + const lineHeight = window.editor.defaultTextHeight() + for (i = 0; i < lines.length; i++) { + const str = lines[i] + + _lineHeightMap.push(acc) + + if (str.length === 0) { + acc++ + continue } - _lineHeightMap.push(acc); - linesCount = acc; - for (i = 0; i < linesCount; i++) { - _scrollMap.push(-1); - } + const h = window.editor.heightAtLine(i + 1) - window.editor.heightAtLine(i) + acc += Math.round(h / lineHeight) + } + _lineHeightMap.push(acc) + linesCount = acc + + for (i = 0; i < linesCount; i++) { + _scrollMap.push(-1) + } - nonEmptyList.push(0); + nonEmptyList.push(0) // make the first line go top - _scrollMap[0] = viewTop; - - const parts = markdownArea.find('.part').toArray(); - for (i = 0; i < parts.length; i++) { - const $el = $(parts[i]); - let t = $el.attr('data-startline') - 1; - if (t === '') { - return; - } - t = _lineHeightMap[t]; - if (t !== 0 && t !== nonEmptyList[nonEmptyList.length - 1]) { - nonEmptyList.push(t); - } - _scrollMap[t] = Math.round($el.offset().top + offset - 10); + _scrollMap[0] = viewTop + + const parts = markdownArea.find('.part').toArray() + for (i = 0; i < parts.length; i++) { + const $el = $(parts[i]) + let t = $el.attr('data-startline') - 1 + if (t === '') { + return } + t = _lineHeightMap[t] + if (t !== 0 && t !== nonEmptyList[nonEmptyList.length - 1]) { + nonEmptyList.push(t) + } + _scrollMap[t] = Math.round($el.offset().top + offset - 10) + } - nonEmptyList.push(linesCount); - _scrollMap[linesCount] = viewArea[0].scrollHeight; - - pos = 0; - for (i = 1; i < linesCount; i++) { - if (_scrollMap[i] !== -1) { - pos++; - continue; - } + nonEmptyList.push(linesCount) + _scrollMap[linesCount] = viewArea[0].scrollHeight - a = nonEmptyList[pos]; - b = nonEmptyList[pos + 1]; - _scrollMap[i] = Math.round((_scrollMap[b] * (i - a) + _scrollMap[a] * (b - i)) / (b - a)); + pos = 0 + for (i = 1; i < linesCount; i++) { + if (_scrollMap[i] !== -1) { + pos++ + continue } - _scrollMap[0] = 0; + a = nonEmptyList[pos] + b = nonEmptyList[pos + 1] + _scrollMap[i] = Math.round((_scrollMap[b] * (i - a) + _scrollMap[a] * (b - i)) / (b - a)) + } + + _scrollMap[0] = 0 - scrollMap = _scrollMap; - lineHeightMap = _lineHeightMap; + scrollMap = _scrollMap + lineHeightMap = _lineHeightMap - if (loaded && callback) callback(); + if (window.loaded && callback) callback() } // sync view scroll progress to edit -let viewScrollingTimer = null; - -export function syncScrollToEdit(event, preventAnimate) { - if (currentMode != modeType.both || !syncscroll || !editArea) return; - if (preventSyncScrollToEdit) { - if (typeof preventSyncScrollToEdit === 'number') { - preventSyncScrollToEdit--; - } else { - preventSyncScrollToEdit = false; - } - return; - } - if (!scrollMap || !lineHeightMap) { - buildMap(() => { - syncScrollToEdit(event, preventAnimate); - }); - return; - } - if (editScrolling) return; - - const scrollTop = viewArea[0].scrollTop; - let lineIndex = 0; - for (var i = 0, l = scrollMap.length; i < l; i++) { - if (scrollMap[i] > scrollTop) { - break; - } else { - lineIndex = i; - } - } - let lineNo = 0; - let lineDiff = 0; - for (var i = 0, l = lineHeightMap.length; i < l; i++) { - if (lineHeightMap[i] > lineIndex) { - break; - } else { - lineNo = lineHeightMap[i]; - lineDiff = lineHeightMap[i + 1] - lineNo; - } - } +let viewScrollingTimer = null - 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; - topDiffPercent = (scrollTop - preLastLinePos) / (viewBottom - preLastLinePos); - posToNextDiff = textHeight * topDiffPercent; - posTo += Math.ceil(posToNextDiff); +export function syncScrollToEdit (event, preventAnimate) { + if (window.currentMode !== window.modeType.both || !window.syncscroll || !editArea) return + if (window.preventSyncScrollToEdit) { + if (typeof window.preventSyncScrollToEdit === 'number') { + window.preventSyncScrollToEdit-- } else { - posTo = lineNo * textHeight; - topDiffPercent = (scrollTop - scrollMap[lineNo]) / (scrollMap[lineNo + lineDiff] - scrollMap[lineNo]); - posToNextDiff = textHeight * lineDiff * topDiffPercent; - posTo += Math.ceil(posToNextDiff); + window.preventSyncScrollToEdit = false } - - if (preventAnimate) { - editArea.scrollTop(posTo); + return + } + if (!scrollMap || !lineHeightMap) { + buildMap(() => { + syncScrollToEdit(event, preventAnimate) + }) + return + } + if (editScrolling) return + + const scrollTop = viewArea[0].scrollTop + let lineIndex = 0 + for (let i = 0, l = scrollMap.length; i < l; i++) { + if (scrollMap[i] > scrollTop) { + break } else { - const posDiff = Math.abs(scrollInfo.top - posTo); - var duration = posDiff / 50; - duration = duration >= 100 ? duration : 100; - editArea.stop(true, true).animate({ - scrollTop: posTo - }, duration, "linear"); + lineIndex = i } - - viewScrolling = true; - clearTimeout(viewScrollingTimer); - viewScrollingTimer = setTimeout(viewScrollingTimeoutInner, duration * 1.5); + } + let lineNo = 0 + let lineDiff = 0 + for (let i = 0, l = lineHeightMap.length; i < l; i++) { + if (lineHeightMap[i] > lineIndex) { + break + } else { + lineNo = lineHeightMap[i] + lineDiff = lineHeightMap[i + 1] - lineNo + } + } + + let posTo = 0 + let topDiffPercent = 0 + let posToNextDiff = 0 + const scrollInfo = window.editor.getScrollInfo() + const textHeight = window.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 + topDiffPercent = (scrollTop - preLastLinePos) / (viewBottom - preLastLinePos) + posToNextDiff = textHeight * topDiffPercent + posTo += Math.ceil(posToNextDiff) + } else { + posTo = lineNo * textHeight + topDiffPercent = (scrollTop - scrollMap[lineNo]) / (scrollMap[lineNo + lineDiff] - scrollMap[lineNo]) + posToNextDiff = textHeight * lineDiff * topDiffPercent + posTo += Math.ceil(posToNextDiff) + } + + if (preventAnimate) { + editArea.scrollTop(posTo) + } else { + const posDiff = Math.abs(scrollInfo.top - posTo) + var duration = posDiff / 50 + duration = duration >= 100 ? duration : 100 + editArea.stop(true, true).animate({ + scrollTop: posTo + }, duration, 'linear') + } + + viewScrolling = true + clearTimeout(viewScrollingTimer) + viewScrollingTimer = setTimeout(viewScrollingTimeoutInner, duration * 1.5) } -function viewScrollingTimeoutInner() { - viewScrolling = false; +function viewScrollingTimeoutInner () { + viewScrolling = false } // sync edit scroll progress to view -let editScrollingTimer = null; - -export function syncScrollToView(event, preventAnimate) { - if (currentMode != modeType.both || !syncscroll || !viewArea) return; - if (preventSyncScrollToView) { - if (typeof preventSyncScrollToView === 'number') { - preventSyncScrollToView--; - } else { - preventSyncScrollToView = false; - } - return; - } - if (!scrollMap || !lineHeightMap) { - buildMap(() => { - syncScrollToView(event, preventAnimate); - }); - return; - } - if (viewScrolling) return; - - 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 - const diffToBottom = (scrollInfo.top + scrollInfo.clientHeight) - (scrollInfo.height - textHeight); - if (scrollInfo.height > scrollInfo.clientHeight && diffToBottom > 0) { - topDiffPercent = diffToBottom / textHeight; - posTo = scrollMap[lineNo + 1]; - posToNextDiff = (viewBottom - posTo) * topDiffPercent; - posTo += Math.floor(posToNextDiff); - } else { - topDiffPercent = (scrollInfo.top % textHeight) / textHeight; - posTo = scrollMap[lineNo]; - posToNextDiff = (scrollMap[lineNo + 1] - posTo) * topDiffPercent; - posTo += Math.floor(posToNextDiff); - } +let editScrollingTimer = null - if (preventAnimate) { - viewArea.scrollTop(posTo); +export function syncScrollToView (event, preventAnimate) { + if (window.currentMode !== window.modeType.both || !window.syncscroll || !viewArea) return + if (window.preventSyncScrollToView) { + if (typeof preventSyncScrollToView === 'number') { + window.preventSyncScrollToView-- } else { - const posDiff = Math.abs(viewArea.scrollTop() - posTo); - var duration = posDiff / 50; - duration = duration >= 100 ? duration : 100; - viewArea.stop(true, true).animate({ - scrollTop: posTo - }, duration, "linear"); + window.preventSyncScrollToView = false } - - editScrolling = true; - clearTimeout(editScrollingTimer); - editScrollingTimer = setTimeout(editScrollingTimeoutInner, duration * 1.5); + return + } + if (!scrollMap || !lineHeightMap) { + buildMap(() => { + syncScrollToView(event, preventAnimate) + }) + return + } + if (viewScrolling) return + + let lineNo, posTo + let topDiffPercent, posToNextDiff + const scrollInfo = window.editor.getScrollInfo() + const textHeight = window.editor.defaultTextHeight() + lineNo = Math.floor(scrollInfo.top / textHeight) + // if reach the last line, will start lerp to the bottom + const diffToBottom = (scrollInfo.top + scrollInfo.clientHeight) - (scrollInfo.height - textHeight) + if (scrollInfo.height > scrollInfo.clientHeight && diffToBottom > 0) { + topDiffPercent = diffToBottom / textHeight + posTo = scrollMap[lineNo + 1] + posToNextDiff = (viewBottom - posTo) * topDiffPercent + posTo += Math.floor(posToNextDiff) + } else { + topDiffPercent = (scrollInfo.top % textHeight) / textHeight + posTo = scrollMap[lineNo] + posToNextDiff = (scrollMap[lineNo + 1] - posTo) * topDiffPercent + posTo += Math.floor(posToNextDiff) + } + + if (preventAnimate) { + viewArea.scrollTop(posTo) + } else { + const posDiff = Math.abs(viewArea.scrollTop() - posTo) + var duration = posDiff / 50 + duration = duration >= 100 ? duration : 100 + viewArea.stop(true, true).animate({ + scrollTop: posTo + }, duration, 'linear') + } + + editScrolling = true + clearTimeout(editScrollingTimer) + editScrollingTimer = setTimeout(editScrollingTimeoutInner, duration * 1.5) } -function editScrollingTimeoutInner() { - editScrolling = false; +function editScrollingTimeoutInner () { + editScrolling = false } diff --git a/public/vendor/md-toc.js b/public/vendor/md-toc.js index 200275a5..f93f7921 100755 --- a/public/vendor/md-toc.js +++ b/public/vendor/md-toc.js @@ -1,129 +1,123 @@ +/* eslint-env browser, jquery */ /** * md-toc.js v1.0.2 * https://github.com/yijian166/md-toc.js */ (function (window) { - function Toc(id, options) { - this.el = document.getElementById(id); - if (!this.el) return; - this.options = options || {}; - this.tocLevel = parseInt(options.level) || 0; - this.tocClass = options['class'] || 'toc'; - this.ulClass = options['ulClass']; - this.tocTop = parseInt(options.top) || 0; - this.elChilds = this.el.children; - this.process = options['process']; - if (!this.elChilds.length) return; - this._init(); - } + function Toc (id, options) { + this.el = document.getElementById(id) + if (!this.el) return + this.options = options || {} + this.tocLevel = parseInt(options.level) || 0 + this.tocClass = options['class'] || 'toc' + this.ulClass = options['ulClass'] + this.tocTop = parseInt(options.top) || 0 + this.elChilds = this.el.children + this.process = options['process'] + if (!this.elChilds.length) return + this._init() + } - Toc.prototype._init = function () { - this._collectTitleElements(); - this._createTocContent(); - this._showToc(); - }; + Toc.prototype._init = function () { + this._collectTitleElements() + this._createTocContent() + this._showToc() + } - Toc.prototype._collectTitleElements = function () { - this._elTitlesNames = [], - this.elTitleElements = []; - for (var i = 1; i < 7; i++) { - if (this.el.getElementsByTagName('h' + i).length) { - this._elTitlesNames.push('h' + i); - } - } + Toc.prototype._collectTitleElements = function () { + this._elTitlesNames = [] + this.elTitleElements = [] + for (var i = 1; i < 7; i++) { + if (this.el.getElementsByTagName('h' + i).length) { + this._elTitlesNames.push('h' + i) + } + } - this._elTitlesNames.length = this._elTitlesNames.length > this.tocLevel ? this.tocLevel : this._elTitlesNames.length; + this._elTitlesNames.length = this._elTitlesNames.length > this.tocLevel ? this.tocLevel : this._elTitlesNames.length - for (var j = 0; j < this.elChilds.length; j++) { - this._elChildName = this.elChilds[j].tagName.toLowerCase(); - if (this._elTitlesNames.toString().match(this._elChildName)) { - this.elTitleElements.push(this.elChilds[j]); - } - } - }; + for (var j = 0; j < this.elChilds.length; j++) { + this._elChildName = this.elChilds[j].tagName.toLowerCase() + if (this._elTitlesNames.toString().match(this._elChildName)) { + this.elTitleElements.push(this.elChilds[j]) + } + } + } - Toc.prototype._createTocContent = function () { - this._elTitleElementsLen = this.elTitleElements.length; - if (!this._elTitleElementsLen) return; - this.tocContent = ''; - this._tempLists = []; + Toc.prototype._createTocContent = function () { + this._elTitleElementsLen = this.elTitleElements.length + if (!this._elTitleElementsLen) return + this.tocContent = '' + this._tempLists = [] - var url = location.origin + location.pathname; - for (var i = 0; i < this._elTitleElementsLen; i++) { - var j = i + 1; - this._elTitleElement = this.elTitleElements[i]; - this._elTitleElementName = this._elTitleElement.tagName; - this._elTitleElementText = (typeof this.process === 'function' ? this.process(this._elTitleElement) : this._elTitleElement.innerHTML).replace(/<(?:.|\n)*?>/gm, ''); - var id = this._elTitleElement.getAttribute('id'); - if (!id) { - this._elTitleElement.setAttribute('id', 'tip' + i); - id = '#tip' + i; - } else { - id = '#' + id; - } + for (var i = 0; i < this._elTitleElementsLen; i++) { + var j = i + 1 + this._elTitleElement = this.elTitleElements[i] + this._elTitleElementName = this._elTitleElement.tagName + this._elTitleElementText = (typeof this.process === 'function' ? this.process(this._elTitleElement) : this._elTitleElement.innerHTML).replace(/<(?:.|\n)*?>/gm, '') + var id = this._elTitleElement.getAttribute('id') + if (!id) { + this._elTitleElement.setAttribute('id', 'tip' + i) + id = '#tip' + i + } else { + id = '#' + id + } - this.tocContent += '<li><a href="' + id + '">' + this._elTitleElementText + '</a>'; + this.tocContent += '<li><a href="' + id + '">' + this._elTitleElementText + '</a>' - if (j != this._elTitleElementsLen) { - this._elNextTitleElementName = this.elTitleElements[j].tagName; - if (this._elTitleElementName != this._elNextTitleElementName) { - var checkColse = false, - y = 1; - for (var t = this._tempLists.length - 1; t >= 0; t--) { - if (this._tempLists[t].tagName == this._elNextTitleElementName) { - checkColse = true; - break; - } - y++; - } - if (checkColse) { - this.tocContent += new Array(y + 1).join('</li></ul>'); - this._tempLists.length = this._tempLists.length - y; - } else { - this._tempLists.push(this._elTitleElement); - if (this.ulClass) - this.tocContent += '<ul class="' + this.ulClass + '">'; - else - this.tocContent += '<ul>'; - } - } else { - this.tocContent += '</li>'; - } - } else { - if (this._tempLists.length) { - this.tocContent += new Array(this._tempLists.length + 1).join('</li></ul>'); - } else { - this.tocContent += '</li>'; - } + if (j !== this._elTitleElementsLen) { + this._elNextTitleElementName = this.elTitleElements[j].tagName + if (this._elTitleElementName !== this._elNextTitleElementName) { + var checkColse = false + var y = 1 + for (var t = this._tempLists.length - 1; t >= 0; t--) { + if (this._tempLists[t].tagName === this._elNextTitleElementName) { + checkColse = true + break } + y++ + } + if (checkColse) { + this.tocContent += new Array(y + 1).join('</li></ul>') + this._tempLists.length = this._tempLists.length - y + } else { + this._tempLists.push(this._elTitleElement) + if (this.ulClass) { this.tocContent += '<ul class="' + this.ulClass + '">' } else { this.tocContent += '<ul>' } + } + } else { + this.tocContent += '</li>' } - if (this.ulClass) - this.tocContent = '<ul class="' + this.ulClass + '">' + this.tocContent + '</ul>'; - else - this.tocContent = '<ul>' + this.tocContent + '</ul>'; - }; - - Toc.prototype._showToc = function () { - this.toc = document.createElement('div'); - this.toc.innerHTML = this.tocContent; - this.toc.setAttribute('class', this.tocClass); - if (!this.options.targetId) { - this.el.appendChild(this.toc); + } else { + if (this._tempLists.length) { + this.tocContent += new Array(this._tempLists.length + 1).join('</li></ul>') } else { - document.getElementById(this.options.targetId).appendChild(this.toc); + this.tocContent += '</li>' } - var self = this; - if (this.tocTop > -1) { - window.onscroll = function () { - var t = document.documentElement.scrollTop || document.body.scrollTop; - if (t < self.tocTop) { - self.toc.setAttribute('style', 'position:absolute;top:' + self.tocTop + 'px;'); - } else { - self.toc.setAttribute('style', 'position:fixed;top:10px;'); - } - } + } + } + if (this.ulClass) { this.tocContent = '<ul class="' + this.ulClass + '">' + this.tocContent + '</ul>' } else { this.tocContent = '<ul>' + this.tocContent + '</ul>' } + } + + Toc.prototype._showToc = function () { + this.toc = document.createElement('div') + this.toc.innerHTML = this.tocContent + this.toc.setAttribute('class', this.tocClass) + if (!this.options.targetId) { + this.el.appendChild(this.toc) + } else { + document.getElementById(this.options.targetId).appendChild(this.toc) + } + var self = this + if (this.tocTop > -1) { + window.onscroll = function () { + var t = document.documentElement.scrollTop || document.body.scrollTop + if (t < self.tocTop) { + self.toc.setAttribute('style', 'position:absolute;top:' + self.tocTop + 'px;') + } else { + self.toc.setAttribute('style', 'position:fixed;top:10px;') } - }; - window.Toc = Toc; -})(window);
\ No newline at end of file + } + } + } + window.Toc = Toc +})(window) diff --git a/webpack.config.js b/webpack.config.js index 236490b9..f9f0a1c9 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,33 +1,33 @@ -var baseConfig = require('./webpackBaseConfig'); -var ExtractTextPlugin = require("extract-text-webpack-plugin"); -var path = require('path'); +var baseConfig = require('./webpackBaseConfig') +var ExtractTextPlugin = require('extract-text-webpack-plugin') +var path = require('path') module.exports = [Object.assign({}, baseConfig, { - plugins: baseConfig.plugins.concat([ - new ExtractTextPlugin("[name].css") - ]) + plugins: baseConfig.plugins.concat([ + new ExtractTextPlugin('[name].css') + ]) }), { - entry: { - htmlExport: path.join(__dirname, 'public/js/htmlExport.js') - }, - module: { - loaders: [{ - test: /\.css$/, - loader: ExtractTextPlugin.extract('style-loader', 'css-loader') - }, { - test: /\.scss$/, - loader: ExtractTextPlugin.extract('style-loader', 'sass-loader') - }, { - test: /\.less$/, - loader: ExtractTextPlugin.extract('style-loader', 'less-loader') - }] - }, - output: { - path: path.join(__dirname, 'public/build'), - publicPath: '/build/', - filename: '[name].js' - }, - plugins: [ - new ExtractTextPlugin("html.min.css") - ] -}]; + entry: { + htmlExport: path.join(__dirname, 'public/js/htmlExport.js') + }, + module: { + loaders: [{ + test: /\.css$/, + loader: ExtractTextPlugin.extract('style-loader', 'css-loader') + }, { + test: /\.scss$/, + loader: ExtractTextPlugin.extract('style-loader', 'sass-loader') + }, { + test: /\.less$/, + loader: ExtractTextPlugin.extract('style-loader', 'less-loader') + }] + }, + output: { + path: path.join(__dirname, 'public/build'), + publicPath: '/build/', + filename: '[name].js' + }, + plugins: [ + new ExtractTextPlugin('html.min.css') + ] +}] diff --git a/webpack.production.js b/webpack.production.js index 7c690d28..7b42843a 100644 --- a/webpack.production.js +++ b/webpack.production.js @@ -1,63 +1,63 @@ -var baseConfig = require('./webpackBaseConfig'); -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'); +var baseConfig = require('./webpackBaseConfig') +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([ - new webpack.DefinePlugin({ - 'process.env': { - 'NODE_ENV': JSON.stringify('production') - } - }), - new ParallelUglifyPlugin({ - uglifyJS: { - compress: { - warnings: false - }, - mangle: false, - sourceMap: false - } - }), - new ExtractTextPlugin("[name].[hash].css") - ]), + plugins: baseConfig.plugins.concat([ + new webpack.DefinePlugin({ + 'process.env': { + 'NODE_ENV': JSON.stringify('production') + } + }), + new ParallelUglifyPlugin({ + uglifyJS: { + compress: { + warnings: false + }, + mangle: false, + sourceMap: false + } + }), + new ExtractTextPlugin('[name].[hash].css') + ]), - output: { - path: path.join(__dirname, 'public/build'), - publicPath: '/build/', - filename: '[id].[name].[hash].js', - baseUrl: '<%- url %>' - } + output: { + path: path.join(__dirname, 'public/build'), + publicPath: '/build/', + filename: '[id].[name].[hash].js', + baseUrl: '<%- url %>' + } }), { - entry: { - htmlExport: path.join(__dirname, 'public/js/htmlExport.js') - }, - module: { - loaders: [{ - test: /\.css$/, - loader: ExtractTextPlugin.extract('style-loader', 'css-loader') - }, { - test: /\.scss$/, - loader: ExtractTextPlugin.extract('style-loader', 'sass-loader') - }, { - test: /\.less$/, - loader: ExtractTextPlugin.extract('style-loader', 'less-loader') - }] - }, - output: { - path: path.join(__dirname, 'public/build'), - publicPath: '/build/', - filename: '[name].js' - }, - plugins: [ - new webpack.DefinePlugin({ - 'process.env': { - 'NODE_ENV': JSON.stringify('production') - } - }), - new ExtractTextPlugin("html.min.css"), - new OptimizeCssAssetsPlugin() - ] -}]; + entry: { + htmlExport: path.join(__dirname, 'public/js/htmlExport.js') + }, + module: { + loaders: [{ + test: /\.css$/, + loader: ExtractTextPlugin.extract('style-loader', 'css-loader') + }, { + test: /\.scss$/, + loader: ExtractTextPlugin.extract('style-loader', 'sass-loader') + }, { + test: /\.less$/, + loader: ExtractTextPlugin.extract('style-loader', 'less-loader') + }] + }, + output: { + path: path.join(__dirname, 'public/build'), + publicPath: '/build/', + filename: '[name].js' + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env': { + 'NODE_ENV': JSON.stringify('production') + } + }), + new ExtractTextPlugin('html.min.css'), + new OptimizeCssAssetsPlugin() + ] +}] diff --git a/webpackBaseConfig.js b/webpackBaseConfig.js index 12ccbf87..9ab4c06a 100644 --- a/webpackBaseConfig.js +++ b/webpackBaseConfig.js @@ -1,441 +1,439 @@ -var webpack = require('webpack'); -var path = require('path'); -var ExtractTextPlugin = require("extract-text-webpack-plugin"); -var HtmlWebpackPlugin = require('html-webpack-plugin'); -var CopyWebpackPlugin = require('copy-webpack-plugin'); +var webpack = require('webpack') +var path = require('path') +var ExtractTextPlugin = require('extract-text-webpack-plugin') +var HtmlWebpackPlugin = require('html-webpack-plugin') +var CopyWebpackPlugin = require('copy-webpack-plugin') module.exports = { - plugins: [ - new webpack.ProvidePlugin({ - Visibility: "visibilityjs", - Cookies: "js-cookie", - key: "keymaster", - $: "jquery", - jQuery: "jquery", - "window.jQuery": "jquery", - "moment": "moment", - "Handlebars": "handlebars" - }), - new webpack.optimize.OccurrenceOrderPlugin(true), - new webpack.optimize.CommonsChunkPlugin({ - names: ["cover", "index", "pretty", "slide", "vendor"], - children: true, - async: true, - filename: '[name].js', - minChunks: Infinity - }), - new HtmlWebpackPlugin({ - template: 'public/views/includes/header.ejs', - chunks: ['font', 'index-styles', 'index'], - filename: path.join(__dirname, 'public/views/build/index-header.ejs'), - inject: false - }), - new HtmlWebpackPlugin({ - template: 'public/views/includes/header.ejs', - chunks: ['font-pack', 'index-styles-pack', 'index-styles', 'index'], - filename: path.join(__dirname, 'public/views/build/index-pack-header.ejs'), - inject: false - }), - new HtmlWebpackPlugin({ - template: 'public/views/includes/scripts.ejs', - chunks: ['index'], - filename: path.join(__dirname, 'public/views/build/index-scripts.ejs'), - inject: false - }), - new HtmlWebpackPlugin({ - template: 'public/views/includes/scripts.ejs', - chunks: ['common', 'index-pack'], - filename: path.join(__dirname, 'public/views/build/index-pack-scripts.ejs'), - inject: false - }), - new HtmlWebpackPlugin({ - template: 'public/views/includes/header.ejs', - chunks: ['font', 'cover'], - filename: path.join(__dirname, 'public/views/build/cover-header.ejs'), - inject: false - }), - new HtmlWebpackPlugin({ - template: 'public/views/includes/header.ejs', - chunks: ['font-pack', 'cover-styles-pack', 'cover'], - filename: path.join(__dirname, 'public/views/build/cover-pack-header.ejs'), - inject: false - }), - new HtmlWebpackPlugin({ - template: 'public/views/includes/scripts.ejs', - chunks: ['cover'], - filename: path.join(__dirname, 'public/views/build/cover-scripts.ejs'), - inject: false - }), - new HtmlWebpackPlugin({ - template: 'public/views/includes/scripts.ejs', - chunks: ['common', 'cover-pack'], - filename: path.join(__dirname, 'public/views/build/cover-pack-scripts.ejs'), - inject: false - }), - new HtmlWebpackPlugin({ - template: 'public/views/includes/header.ejs', - chunks: ['font', 'pretty-styles', 'pretty'], - filename: path.join(__dirname, 'public/views/build/pretty-header.ejs'), - inject: false - }), - new HtmlWebpackPlugin({ - template: 'public/views/includes/header.ejs', - chunks: ['font-pack', 'pretty-styles-pack', 'pretty-styles', 'pretty'], - filename: path.join(__dirname, 'public/views/build/pretty-pack-header.ejs'), - inject: false - }), - new HtmlWebpackPlugin({ - template: 'public/views/includes/scripts.ejs', - chunks: ['pretty'], - filename: path.join(__dirname, 'public/views/build/pretty-scripts.ejs'), - inject: false - }), - new HtmlWebpackPlugin({ - template: 'public/views/includes/scripts.ejs', - chunks: ['common', 'pretty-pack'], - filename: path.join(__dirname, 'public/views/build/pretty-pack-scripts.ejs'), - inject: false - }), - new HtmlWebpackPlugin({ - template: 'public/views/includes/header.ejs', - chunks: ['font', 'slide-styles', 'slide'], - filename: path.join(__dirname, 'public/views/build/slide-header.ejs'), - inject: false - }), - new HtmlWebpackPlugin({ - template: 'public/views/includes/header.ejs', - chunks: ['font-pack', 'slide-styles-pack', 'slide-styles', 'slide'], - filename: path.join(__dirname, 'public/views/build/slide-pack-header.ejs'), - inject: false - }), - new HtmlWebpackPlugin({ - template: 'public/views/includes/scripts.ejs', - chunks: ['slide'], - filename: path.join(__dirname, 'public/views/build/slide-scripts.ejs'), - inject: false - }), - new HtmlWebpackPlugin({ - template: 'public/views/includes/scripts.ejs', - chunks: ['slide-pack'], - filename: path.join(__dirname, 'public/views/build/slide-pack-scripts.ejs'), - inject: false - }), - new CopyWebpackPlugin([ - { - context: path.join(__dirname, 'node_modules/mathjax'), - from: { - glob: '**/*', - dot: false - }, - to: 'MathJax/' - }, - { - context: path.join(__dirname, 'node_modules/emojify.js'), - from: { - glob: 'dist/**/*', - dot: false - }, - to: 'emojify.js/' - }, - { - context: path.join(__dirname, 'node_modules/reveal.js'), - from: 'js', - to: 'reveal.js/js' - }, - { - context: path.join(__dirname, 'node_modules/reveal.js'), - from: 'css', - to: 'reveal.js/css' - }, - { - context: path.join(__dirname, 'node_modules/reveal.js'), - from: 'lib', - to: 'reveal.js/lib' - }, - { - context: path.join(__dirname, 'node_modules/reveal.js'), - from: 'plugin', - to: 'reveal.js/plugin' - } - ]) + plugins: [ + new webpack.ProvidePlugin({ + Visibility: 'visibilityjs', + Cookies: 'js-cookie', + key: 'keymaster', + $: 'jquery', + jQuery: 'jquery', + 'window.jQuery': 'jquery', + 'moment': 'moment', + 'Handlebars': 'handlebars' + }), + new webpack.optimize.OccurrenceOrderPlugin(true), + new webpack.optimize.CommonsChunkPlugin({ + names: ['cover', 'index', 'pretty', 'slide', 'vendor'], + children: true, + async: true, + filename: '[name].js', + minChunks: Infinity + }), + new HtmlWebpackPlugin({ + template: 'public/views/includes/header.ejs', + chunks: ['font', 'index-styles', 'index'], + filename: path.join(__dirname, 'public/views/build/index-header.ejs'), + inject: false + }), + new HtmlWebpackPlugin({ + template: 'public/views/includes/header.ejs', + chunks: ['font-pack', 'index-styles-pack', 'index-styles', 'index'], + filename: path.join(__dirname, 'public/views/build/index-pack-header.ejs'), + inject: false + }), + new HtmlWebpackPlugin({ + template: 'public/views/includes/scripts.ejs', + chunks: ['index'], + filename: path.join(__dirname, 'public/views/build/index-scripts.ejs'), + inject: false + }), + new HtmlWebpackPlugin({ + template: 'public/views/includes/scripts.ejs', + chunks: ['common', 'index-pack'], + filename: path.join(__dirname, 'public/views/build/index-pack-scripts.ejs'), + inject: false + }), + new HtmlWebpackPlugin({ + template: 'public/views/includes/header.ejs', + chunks: ['font', 'cover'], + filename: path.join(__dirname, 'public/views/build/cover-header.ejs'), + inject: false + }), + new HtmlWebpackPlugin({ + template: 'public/views/includes/header.ejs', + chunks: ['font-pack', 'cover-styles-pack', 'cover'], + filename: path.join(__dirname, 'public/views/build/cover-pack-header.ejs'), + inject: false + }), + new HtmlWebpackPlugin({ + template: 'public/views/includes/scripts.ejs', + chunks: ['cover'], + filename: path.join(__dirname, 'public/views/build/cover-scripts.ejs'), + inject: false + }), + new HtmlWebpackPlugin({ + template: 'public/views/includes/scripts.ejs', + chunks: ['common', 'cover-pack'], + filename: path.join(__dirname, 'public/views/build/cover-pack-scripts.ejs'), + inject: false + }), + new HtmlWebpackPlugin({ + template: 'public/views/includes/header.ejs', + chunks: ['font', 'pretty-styles', 'pretty'], + filename: path.join(__dirname, 'public/views/build/pretty-header.ejs'), + inject: false + }), + new HtmlWebpackPlugin({ + template: 'public/views/includes/header.ejs', + chunks: ['font-pack', 'pretty-styles-pack', 'pretty-styles', 'pretty'], + filename: path.join(__dirname, 'public/views/build/pretty-pack-header.ejs'), + inject: false + }), + new HtmlWebpackPlugin({ + template: 'public/views/includes/scripts.ejs', + chunks: ['pretty'], + filename: path.join(__dirname, 'public/views/build/pretty-scripts.ejs'), + inject: false + }), + new HtmlWebpackPlugin({ + template: 'public/views/includes/scripts.ejs', + chunks: ['common', 'pretty-pack'], + filename: path.join(__dirname, 'public/views/build/pretty-pack-scripts.ejs'), + inject: false + }), + new HtmlWebpackPlugin({ + template: 'public/views/includes/header.ejs', + chunks: ['font', 'slide-styles', 'slide'], + filename: path.join(__dirname, 'public/views/build/slide-header.ejs'), + inject: false + }), + new HtmlWebpackPlugin({ + template: 'public/views/includes/header.ejs', + chunks: ['font-pack', 'slide-styles-pack', 'slide-styles', 'slide'], + filename: path.join(__dirname, 'public/views/build/slide-pack-header.ejs'), + inject: false + }), + new HtmlWebpackPlugin({ + template: 'public/views/includes/scripts.ejs', + chunks: ['slide'], + filename: path.join(__dirname, 'public/views/build/slide-scripts.ejs'), + inject: false + }), + new HtmlWebpackPlugin({ + template: 'public/views/includes/scripts.ejs', + chunks: ['slide-pack'], + filename: path.join(__dirname, 'public/views/build/slide-pack-scripts.ejs'), + inject: false + }), + new CopyWebpackPlugin([ + { + context: path.join(__dirname, 'node_modules/mathjax'), + from: { + glob: '**/*', + dot: false + }, + to: 'MathJax/' + }, + { + context: path.join(__dirname, 'node_modules/emojify.js'), + from: { + glob: 'dist/**/*', + dot: false + }, + to: 'emojify.js/' + }, + { + context: path.join(__dirname, 'node_modules/reveal.js'), + from: 'js', + to: 'reveal.js/js' + }, + { + context: path.join(__dirname, 'node_modules/reveal.js'), + from: 'css', + to: 'reveal.js/css' + }, + { + context: path.join(__dirname, 'node_modules/reveal.js'), + from: 'lib', + to: 'reveal.js/lib' + }, + { + context: path.join(__dirname, 'node_modules/reveal.js'), + from: 'plugin', + to: 'reveal.js/plugin' + } + ]) + ], + entry: { + font: path.join(__dirname, 'public/css/google-font.css'), + 'font-pack': path.join(__dirname, 'public/css/font.css'), + common: [ + 'expose?jQuery!expose?$!jquery', + 'velocity-animate', + 'imports?$=jquery!jquery-mousewheel', + 'bootstrap' ], + cover: [ + 'babel-polyfill', + path.join(__dirname, 'public/js/cover.js') + ], + 'cover-styles-pack': [ + path.join(__dirname, 'node_modules/bootstrap/dist/css/bootstrap.min.css'), + path.join(__dirname, 'node_modules/font-awesome/css/font-awesome.min.css'), + path.join(__dirname, 'public/css/bootstrap-social.css'), + path.join(__dirname, 'node_modules/select2/select2.css'), + path.join(__dirname, 'node_modules/select2/select2-bootstrap.css') + ], + 'cover-pack': [ + 'babel-polyfill', + 'bootstrap-validator', + 'script!listPagnation', + 'expose?select2!select2', + 'expose?moment!moment', + 'script!js-url', + path.join(__dirname, 'public/js/cover.js') + ], + index: [ + 'babel-polyfill', + 'script!jquery-ui-resizable', + 'script!js-url', + 'expose?filterXSS!xss', + 'script!Idle.Js', + 'expose?LZString!lz-string', + 'script!codemirror', + 'script!inlineAttachment', + 'script!jqueryTextcomplete', + 'script!codemirrorSpellChecker', + 'script!codemirrorInlineAttachment', + 'script!ot', + 'flowchart.js', + 'js-sequence-diagrams', + 'expose?RevealMarkdown!reveal-markdown', + path.join(__dirname, 'public/js/google-drive-upload.js'), + path.join(__dirname, 'public/js/google-drive-picker.js'), + path.join(__dirname, 'public/js/index.js') + ], + 'index-styles': [ + path.join(__dirname, 'public/vendor/jquery-ui/jquery-ui.min.css'), + path.join(__dirname, 'public/vendor/codemirror-spell-checker/spell-checker.min.css'), + path.join(__dirname, 'node_modules/codemirror/lib/codemirror.css'), + path.join(__dirname, 'node_modules/codemirror/addon/fold/foldgutter.css'), + path.join(__dirname, 'node_modules/codemirror/addon/display/fullscreen.css'), + path.join(__dirname, 'node_modules/codemirror/addon/dialog/dialog.css'), + path.join(__dirname, 'node_modules/codemirror/addon/scroll/simplescrollbars.css'), + path.join(__dirname, 'node_modules/codemirror/addon/search/matchesonscrollbar.css'), + path.join(__dirname, 'node_modules/codemirror/theme/monokai.css'), + path.join(__dirname, 'node_modules/codemirror/theme/one-dark.css'), + path.join(__dirname, 'node_modules/codemirror/mode/tiddlywiki/tiddlywiki.css'), + path.join(__dirname, 'node_modules/codemirror/mode/mediawiki/mediawiki.css'), + path.join(__dirname, 'public/css/github-extract.css'), + path.join(__dirname, 'public/vendor/showup/showup.css'), + path.join(__dirname, 'public/css/mermaid.css'), + path.join(__dirname, 'public/css/markdown.css'), + path.join(__dirname, 'public/css/slide-preview.css') + ], + 'index-styles-pack': [ + path.join(__dirname, 'node_modules/bootstrap/dist/css/bootstrap.min.css'), + path.join(__dirname, 'node_modules/font-awesome/css/font-awesome.min.css'), + path.join(__dirname, 'public/css/bootstrap-social.css'), + path.join(__dirname, 'node_modules/ionicons/css/ionicons.min.css'), + path.join(__dirname, 'node_modules/octicons/octicons/octicons.css') + ], + 'index-pack': [ + 'babel-polyfill', + 'expose?Spinner!spin.js', + 'script!jquery-ui-resizable', + 'bootstrap-validator', + 'expose?jsyaml!js-yaml', + 'script!mermaid', + 'expose?moment!moment', + 'script!js-url', + 'script!handlebars', + 'expose?hljs!highlight.js', + 'expose?emojify!emojify.js', + 'expose?filterXSS!xss', + 'script!Idle.Js', + 'script!gist-embed', + 'expose?LZString!lz-string', + 'script!codemirror', + 'script!inlineAttachment', + 'script!jqueryTextcomplete', + 'script!codemirrorSpellChecker', + 'script!codemirrorInlineAttachment', + 'script!ot', + 'flowchart.js', + 'js-sequence-diagrams', + 'expose?Viz!viz.js', + 'expose?io!socket.io-client', + 'expose?RevealMarkdown!reveal-markdown', + path.join(__dirname, 'public/js/google-drive-upload.js'), + path.join(__dirname, 'public/js/google-drive-picker.js'), + path.join(__dirname, 'public/js/index.js') + ], + pretty: [ + 'babel-polyfill', + 'expose?filterXSS!xss', + 'flowchart.js', + 'js-sequence-diagrams', + 'expose?RevealMarkdown!reveal-markdown', + path.join(__dirname, 'public/js/pretty.js') + ], + 'pretty-styles': [ + path.join(__dirname, 'public/css/github-extract.css'), + path.join(__dirname, 'public/css/mermaid.css'), + path.join(__dirname, 'public/css/markdown.css'), + path.join(__dirname, 'public/css/slide-preview.css') + ], + 'pretty-styles-pack': [ + path.join(__dirname, 'node_modules/bootstrap/dist/css/bootstrap.min.css'), + path.join(__dirname, 'node_modules/font-awesome/css/font-awesome.min.css'), + path.join(__dirname, 'node_modules/ionicons/css/ionicons.min.css'), + path.join(__dirname, 'node_modules/octicons/octicons/octicons.css') + ], + 'pretty-pack': [ + 'babel-polyfill', + 'expose?jsyaml!js-yaml', + 'script!mermaid', + 'expose?moment!moment', + 'script!handlebars', + 'expose?hljs!highlight.js', + 'expose?emojify!emojify.js', + 'expose?filterXSS!xss', + 'script!gist-embed', + 'flowchart.js', + 'js-sequence-diagrams', + 'expose?Viz!viz.js', + 'expose?RevealMarkdown!reveal-markdown', + path.join(__dirname, 'public/js/pretty.js') + ], + slide: [ + 'babel-polyfill', + 'bootstrap-tooltip', + 'expose?filterXSS!xss', + 'flowchart.js', + 'js-sequence-diagrams', + 'expose?RevealMarkdown!reveal-markdown', + path.join(__dirname, 'public/js/slide.js') + ], + 'slide-styles': [ + path.join(__dirname, 'public/vendor/bootstrap/tooltip.min.css'), + path.join(__dirname, 'public/css/github-extract.css'), + path.join(__dirname, 'public/css/mermaid.css'), + path.join(__dirname, 'public/css/markdown.css') + ], + 'slide-styles-pack': [ + path.join(__dirname, 'node_modules/font-awesome/css/font-awesome.min.css'), + path.join(__dirname, 'node_modules/ionicons/css/ionicons.min.css'), + path.join(__dirname, 'node_modules/octicons/octicons/octicons.css') + ], + 'slide-pack': [ + 'babel-polyfill', + 'expose?jQuery!expose?$!jquery', + 'velocity-animate', + 'imports?$=jquery!jquery-mousewheel', + 'bootstrap-tooltip', + 'expose?jsyaml!js-yaml', + 'script!mermaid', + 'expose?moment!moment', + 'script!handlebars', + 'expose?hljs!highlight.js', + 'expose?emojify!emojify.js', + 'expose?filterXSS!xss', + 'script!gist-embed', + 'flowchart.js', + 'js-sequence-diagrams', + 'expose?Viz!viz.js', + 'headjs', + 'expose?Reveal!reveal.js', + 'expose?RevealMarkdown!reveal-markdown', + path.join(__dirname, 'public/js/slide.js') + ] + }, - entry: { - font: path.join(__dirname, 'public/css/google-font.css'), - "font-pack": path.join(__dirname, 'public/css/font.css'), - common: [ - "expose?jQuery!expose?$!jquery", - "velocity-animate", - "imports?$=jquery!jquery-mousewheel", - "bootstrap" - ], - cover: [ - "babel-polyfill", - path.join(__dirname, 'public/js/cover.js') - ], - "cover-styles-pack": [ - path.join(__dirname, 'node_modules/bootstrap/dist/css/bootstrap.min.css'), - path.join(__dirname, 'node_modules/font-awesome/css/font-awesome.min.css'), - path.join(__dirname, 'public/css/bootstrap-social.css'), - path.join(__dirname, 'node_modules/select2/select2.css'), - path.join(__dirname, 'node_modules/select2/select2-bootstrap.css'), - ], - "cover-pack": [ - "babel-polyfill", - "bootstrap-validator", - "script!listPagnation", - "expose?select2!select2", - "expose?moment!moment", - "script!js-url", - path.join(__dirname, 'public/js/cover.js') - ], - index: [ - "babel-polyfill", - "script!jquery-ui-resizable", - "script!js-url", - "expose?filterXSS!xss", - "script!Idle.Js", - "expose?LZString!lz-string", - "script!codemirror", - "script!inlineAttachment", - "script!jqueryTextcomplete", - "script!codemirrorSpellChecker", - "script!codemirrorInlineAttachment", - "script!ot", - "flowchart.js", - "js-sequence-diagrams", - "expose?RevealMarkdown!reveal-markdown", - path.join(__dirname, 'public/js/google-drive-upload.js'), - path.join(__dirname, 'public/js/google-drive-picker.js'), - path.join(__dirname, 'public/js/index.js') - ], - "index-styles": [ - path.join(__dirname, 'public/vendor/jquery-ui/jquery-ui.min.css'), - path.join(__dirname, 'public/vendor/codemirror-spell-checker/spell-checker.min.css'), - path.join(__dirname, 'node_modules/codemirror/lib/codemirror.css'), - path.join(__dirname, 'node_modules/codemirror/addon/fold/foldgutter.css'), - path.join(__dirname, 'node_modules/codemirror/addon/display/fullscreen.css'), - path.join(__dirname, 'node_modules/codemirror/addon/dialog/dialog.css'), - path.join(__dirname, 'node_modules/codemirror/addon/scroll/simplescrollbars.css'), - path.join(__dirname, 'node_modules/codemirror/addon/search/matchesonscrollbar.css'), - path.join(__dirname, 'node_modules/codemirror/theme/monokai.css'), - path.join(__dirname, 'node_modules/codemirror/theme/one-dark.css'), - path.join(__dirname, 'node_modules/codemirror/mode/tiddlywiki/tiddlywiki.css'), - path.join(__dirname, 'node_modules/codemirror/mode/mediawiki/mediawiki.css'), - path.join(__dirname, 'public/css/github-extract.css'), - path.join(__dirname, 'public/vendor/showup/showup.css'), - path.join(__dirname, 'public/css/mermaid.css'), - path.join(__dirname, 'public/css/markdown.css'), - path.join(__dirname, 'public/css/slide-preview.css') - ], - "index-styles-pack": [ - path.join(__dirname, 'node_modules/bootstrap/dist/css/bootstrap.min.css'), - path.join(__dirname, 'node_modules/font-awesome/css/font-awesome.min.css'), - path.join(__dirname, 'public/css/bootstrap-social.css'), - path.join(__dirname, 'node_modules/ionicons/css/ionicons.min.css'), - path.join(__dirname, 'node_modules/octicons/octicons/octicons.css') - ], - "index-pack": [ - "babel-polyfill", - "expose?Spinner!spin.js", - "script!jquery-ui-resizable", - "bootstrap-validator", - "expose?jsyaml!js-yaml", - "script!mermaid", - "expose?moment!moment", - "script!js-url", - "script!handlebars", - "expose?hljs!highlight.js", - "expose?emojify!emojify.js", - "expose?filterXSS!xss", - "script!Idle.Js", - "script!gist-embed", - "expose?LZString!lz-string", - "script!codemirror", - "script!inlineAttachment", - "script!jqueryTextcomplete", - "script!codemirrorSpellChecker", - "script!codemirrorInlineAttachment", - "script!ot", - "flowchart.js", - "js-sequence-diagrams", - "expose?Viz!viz.js", - "expose?io!socket.io-client", - "expose?RevealMarkdown!reveal-markdown", - path.join(__dirname, 'public/js/google-drive-upload.js'), - path.join(__dirname, 'public/js/google-drive-picker.js'), - path.join(__dirname, 'public/js/index.js') - ], - pretty: [ - "babel-polyfill", - "expose?filterXSS!xss", - "flowchart.js", - "js-sequence-diagrams", - "expose?RevealMarkdown!reveal-markdown", - path.join(__dirname, 'public/js/pretty.js') - ], - "pretty-styles": [ - path.join(__dirname, 'public/css/github-extract.css'), - path.join(__dirname, 'public/css/mermaid.css'), - path.join(__dirname, 'public/css/markdown.css'), - path.join(__dirname, 'public/css/slide-preview.css') - ], - "pretty-styles-pack": [ - path.join(__dirname, 'node_modules/bootstrap/dist/css/bootstrap.min.css'), - path.join(__dirname, 'node_modules/font-awesome/css/font-awesome.min.css'), - path.join(__dirname, 'node_modules/ionicons/css/ionicons.min.css'), - path.join(__dirname, 'node_modules/octicons/octicons/octicons.css') - ], - "pretty-pack": [ - "babel-polyfill", - "expose?jsyaml!js-yaml", - "script!mermaid", - "expose?moment!moment", - "script!handlebars", - "expose?hljs!highlight.js", - "expose?emojify!emojify.js", - "expose?filterXSS!xss", - "script!gist-embed", - "flowchart.js", - "js-sequence-diagrams", - "expose?Viz!viz.js", - "expose?RevealMarkdown!reveal-markdown", - path.join(__dirname, 'public/js/pretty.js') - ], - slide: [ - "babel-polyfill", - "bootstrap-tooltip", - "expose?filterXSS!xss", - "flowchart.js", - "js-sequence-diagrams", - "expose?RevealMarkdown!reveal-markdown", - path.join(__dirname, 'public/js/slide.js') - ], - "slide-styles": [ - path.join(__dirname, 'public/vendor/bootstrap/tooltip.min.css'), - path.join(__dirname, 'public/css/github-extract.css'), - path.join(__dirname, 'public/css/mermaid.css'), - path.join(__dirname, 'public/css/markdown.css') - ], - "slide-styles-pack": [ - path.join(__dirname, 'node_modules/font-awesome/css/font-awesome.min.css'), - path.join(__dirname, 'node_modules/ionicons/css/ionicons.min.css'), - path.join(__dirname, 'node_modules/octicons/octicons/octicons.css') - ], - "slide-pack": [ - "babel-polyfill", - "expose?jQuery!expose?$!jquery", - "velocity-animate", - "imports?$=jquery!jquery-mousewheel", - "bootstrap-tooltip", - "expose?jsyaml!js-yaml", - "script!mermaid", - "expose?moment!moment", - "script!handlebars", - "expose?hljs!highlight.js", - "expose?emojify!emojify.js", - "expose?filterXSS!xss", - "script!gist-embed", - "flowchart.js", - "js-sequence-diagrams", - "expose?Viz!viz.js", - "headjs", - "expose?Reveal!reveal.js", - "expose?RevealMarkdown!reveal-markdown", - path.join(__dirname, 'public/js/slide.js') - ] - }, - - output: { - path: path.join(__dirname, 'public/build'), - publicPath: '/build/', - filename: '[name].js', - baseUrl: '<%- url %>' - }, - - resolve: { - modulesDirectories: [ - path.resolve(__dirname, 'src'), - path.resolve(__dirname, 'node_modules') - ], - extensions: ["", ".js"], - alias: { - codemirror: path.join(__dirname, 'node_modules/codemirror/codemirror.min.js'), - inlineAttachment: path.join(__dirname, 'public/vendor/inlineAttachment/inline-attachment.js'), - jqueryTextcomplete: path.join(__dirname, 'public/vendor/jquery-textcomplete/jquery.textcomplete.js'), - codemirrorSpellChecker: path.join(__dirname, 'public/vendor/codemirror-spell-checker/spell-checker.min.js'), - codemirrorInlineAttachment: path.join(__dirname, 'public/vendor/inlineAttachment/codemirror.inline-attachment.js'), - ot: path.join(__dirname, 'public/vendor/ot/ot.min.js'), - listPagnation: path.join(__dirname, 'node_modules/list.pagination.js/dist/list.pagination.min.js'), - mermaid: path.join(__dirname, 'node_modules/mermaid/dist/mermaid.min.js'), - handlebars: path.join(__dirname, 'node_modules/handlebars/dist/handlebars.min.js'), - "jquery-ui-resizable": path.join(__dirname, 'public/vendor/jquery-ui/jquery-ui.min.js'), - "gist-embed": path.join(__dirname, 'node_modules/gist-embed/gist-embed.min.js'), - "bootstrap-tooltip": path.join(__dirname, 'public/vendor/bootstrap/tooltip.min.js'), - "headjs": path.join(__dirname, 'node_modules/reveal.js/lib/js/head.min.js'), - "reveal-markdown": path.join(__dirname, 'public/js/reveal-markdown.js') - } - }, + output: { + path: path.join(__dirname, 'public/build'), + publicPath: '/build/', + filename: '[name].js', + baseUrl: '<%- url %>' + }, - externals: { - "viz.js": "Viz", - "socket.io-client": "io", - "lodash": "_", - "jquery": "$", - "moment": "moment", - "handlebars": "Handlebars", - "highlight.js": "hljs", - "select2": "select2" - }, + resolve: { + modulesDirectories: [ + path.resolve(__dirname, 'src'), + path.resolve(__dirname, 'node_modules') + ], + extensions: ['', '.js'], + alias: { + codemirror: path.join(__dirname, 'node_modules/codemirror/codemirror.min.js'), + inlineAttachment: path.join(__dirname, 'public/vendor/inlineAttachment/inline-attachment.js'), + jqueryTextcomplete: path.join(__dirname, 'public/vendor/jquery-textcomplete/jquery.textcomplete.js'), + codemirrorSpellChecker: path.join(__dirname, 'public/vendor/codemirror-spell-checker/spell-checker.min.js'), + codemirrorInlineAttachment: path.join(__dirname, 'public/vendor/inlineAttachment/codemirror.inline-attachment.js'), + ot: path.join(__dirname, 'public/vendor/ot/ot.min.js'), + listPagnation: path.join(__dirname, 'node_modules/list.pagination.js/dist/list.pagination.min.js'), + mermaid: path.join(__dirname, 'node_modules/mermaid/dist/mermaid.min.js'), + handlebars: path.join(__dirname, 'node_modules/handlebars/dist/handlebars.min.js'), + 'jquery-ui-resizable': path.join(__dirname, 'public/vendor/jquery-ui/jquery-ui.min.js'), + 'gist-embed': path.join(__dirname, 'node_modules/gist-embed/gist-embed.min.js'), + 'bootstrap-tooltip': path.join(__dirname, 'public/vendor/bootstrap/tooltip.min.js'), + 'headjs': path.join(__dirname, 'node_modules/reveal.js/lib/js/head.min.js'), + 'reveal-markdown': path.join(__dirname, 'public/js/reveal-markdown.js') + } + }, - module: { - loaders: [{ - test: /\.json$/, - loader: 'json-loader' - }, { - test: /\.js$/, - loader: 'babel', - exclude: [/node_modules/, /public\/vendor/] - }, { - test: /\.css$/, - loader: ExtractTextPlugin.extract('style-loader', 'css-loader') - }, { - test: /\.scss$/, - loader: ExtractTextPlugin.extract('style-loader', 'sass-loader') - }, { - test: /\.less$/, - loader: ExtractTextPlugin.extract('style-loader', 'less-loader') - }, { - test: require.resolve("js-sequence-diagrams"), - loader: "imports?Raphael=raphael" - }, { - test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, - loader: "file" - }, { - test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, - loader: "url?prefix=font/&limit=5000" - }, { - test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, - loader: "url?limit=10000&mimetype=application/octet-stream" - }, { - test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, - loader: "url?limit=10000&mimetype=image/svg+xml" - }, { - test: /\.png(\?v=\d+\.\d+\.\d+)?$/, - loader: "url?limit=10000&mimetype=image/png" - }, { - test: /\.gif(\?v=\d+\.\d+\.\d+)?$/, - loader: "url?limit=10000&mimetype=image/gif" - }] - }, + externals: { + 'viz.js': 'Viz', + 'socket.io-client': 'io', + 'lodash': '_', + 'jquery': '$', + 'moment': 'moment', + 'handlebars': 'Handlebars', + 'highlight.js': 'hljs', + 'select2': 'select2' + }, - node: { - fs: "empty" - }, + module: { + loaders: [{ + test: /\.json$/, + loader: 'json-loader' + }, { + test: /\.js$/, + loader: 'babel', + exclude: [/node_modules/, /public\/vendor/] + }, { + test: /\.css$/, + loader: ExtractTextPlugin.extract('style-loader', 'css-loader') + }, { + test: /\.scss$/, + loader: ExtractTextPlugin.extract('style-loader', 'sass-loader') + }, { + test: /\.less$/, + loader: ExtractTextPlugin.extract('style-loader', 'less-loader') + }, { + test: require.resolve('js-sequence-diagrams'), + loader: 'imports?Raphael=raphael' + }, { + test: /\.eot(\?v=\d+\.\d+\.\d+)?$/, + loader: 'file' + }, { + test: /\.(woff|woff2)(\?v=\d+\.\d+\.\d+)?$/, + loader: 'url?prefix=font/&limit=5000' + }, { + test: /\.ttf(\?v=\d+\.\d+\.\d+)?$/, + loader: 'url?limit=10000&mimetype=application/octet-stream' + }, { + test: /\.svg(\?v=\d+\.\d+\.\d+)?$/, + loader: 'url?limit=10000&mimetype=image/svg+xml' + }, { + test: /\.png(\?v=\d+\.\d+\.\d+)?$/, + loader: 'url?limit=10000&mimetype=image/png' + }, { + test: /\.gif(\?v=\d+\.\d+\.\d+)?$/, + loader: 'url?limit=10000&mimetype=image/gif' + }] + }, + node: { + fs: 'empty' + }, - quiet: false, - noInfo: false, - stats: { - assets: false - } -}; + quiet: false, + noInfo: false, + stats: { + assets: false + } +} |