diff --git a/.travis.yml b/.travis.yml index ed8ab42..235f84b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,7 +2,7 @@ language: node_js node_js: - 6 - 7 - - stable + - lts/boron env: - CXX=g++-4.8 addons: diff --git a/README.md b/README.md index 2afeba2..77aff69 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ HackMD === +[![Standard - JavaScript Style Guide](https://cdn.rawgit.com/feross/standard/master/badge.svg)](https://github.com/feross/standard) + [![Join the chat at https://gitter.im/hackmdio/hackmd][gitter-image]][gitter-url] [![build status][travis-image]][travis-url] diff --git a/app.js b/app.js index 67a6254..c68652b 100644 --- a/app.js +++ b/app.js @@ -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'); +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"); +// 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 +// 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 } - } -}); -//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); - }); + }).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) + }) + } + } +}) -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); - }); -} -//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 -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 -app.post('/uploadimage', function (req, res) { - var form = new formidable.IncomingForm(); - - form.keepExtensions = true; - - 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; - } - } catch (err) { - logger.error(err); - return res.status(500).end('upload image error'); - } + 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 } - }); -}); -//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); + }).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 +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 +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 +app.post('/uploadimage', function (req, res) { + var form = new formidable.IncomingForm() + + form.keepExtensions = true + + 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) { + 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 + } + 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 + } + } 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); + 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; - }); +// 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 4b14e42..ef1d646 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({ +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 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) { + passport.use(new GithubStrategy({ + 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) { + 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) { + passport.use(new GoogleStrategy({ + clientID: config.google.clientID, + clientSecret: config.google.clientSecret, + callbackURL: config.serverurl + '/auth/google/callback' + }, callback)) + } +// 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 + } + }, + 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() + profileid: profile.id.toString() }, defaults: { - profile: stringifiedProfile, - accessToken: accessToken, - refreshToken: refreshToken + profile: stringifiedProfile } - }).spread(function (user, created) { + }).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); - } + 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('auth callback failed: ' + err); - return done(err, null); - }); + }).catch(function (err) { + logger.error('ldap auth failed: ' + err) + return done(err, null) + }) + })) + } +// email + if (config.email) { + passport.use(new LocalStrategy({ + 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) + }) + })) + } } -//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) { - passport.use(new TwitterStrategy({ - 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) { - 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) { - 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) { - passport.use(new GoogleStrategy({ - clientID: config.google.clientID, - clientSecret: config.google.clientSecret, - callbackURL: config.serverurl + '/auth/google/callback' - }, callback)); -} -// 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 - }, - }, - 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, - } - }).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) { - passport.use(new LocalStrategy({ - 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); - }); - })); +module.exports = { + registerAuthMethod: registerAuthMethod } diff --git a/lib/config.js b/lib/config.js index 4d2fbf7..af4c22c 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); +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; +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 e7fb308..69337dc 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 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 getHistory (userid, callback) { + models.User.findOne({ + where: { + id: userid } -} - -function parseHistoryToArray(history) { - var _history = []; - Object.keys(history).forEach(function (key) { - var item = history[key]; - _history.push(item); - }); - return _history; -} - -function parseHistoryToObject(history) { - var _history = {}; - for (var i = 0, l = history.length; i < l; i++) { - var item = history[i]; - _history[item.id] = item; + }).then(function (user) { + if (!user) { + return callback(null, null) } - return _history; + 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 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) - }); - }); +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 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 parseHistoryToArray (history) { + var _history = [] + Object.keys(history).forEach(function (key) { + var item = history[key] + _history.push(item) + }) + return _history +} + +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) + } } 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); - } + 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 3afa03f..92bd36e 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 = ''; - svg += ''; - svg += ''; - svg += ''; - svg += ''; - svg += '' + letter + ''; - svg += ''; - svg += ''; - svg += ''; + var svg = '' + svg += '' + svg += '' + svg += '' + svg += '' + svg += '' + letter + '' + svg += '' + 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 61299c1..23e302d 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 3af490a..20c0e03 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 fa647d9..618bb4d 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 239327e..98381d4 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 92ff6f7..984920b 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 b5aaf77..a97d3be 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 0b0f149..5e39c34 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 e83956e..96babc2 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 8b38d3f..bef9ee2 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); - } - } - }); + return _callback(null, null) } - 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 _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; - } - } - 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); - } - } - return operations; + } + }).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) } - }, - 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; - } - } - } - // 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); - }); + }, + 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 + } + } + } + if (authorship[1] >= opStart) { + authorship[1] += op.length + authorship[2] += op.length + } + } + } + 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 (authorship[1] >= opEnd) { + authorship[1] += op + authorship[2] += op + } + } + } + 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 c7360fe..d8dab30 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'), ""); + 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('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 - }); + 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') } - 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); - }); - }).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); - }); - }).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); - }); + } + } + }, + { + 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) + }) + }).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 Revision; -}; \ No newline at end of file + return Revision +} diff --git a/lib/models/temp.js b/lib/models/temp.js index 6eeff15..e770bb3 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 dd93bf7..f7e533b 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 c1db688..cff795c 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'); +// 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"); +// 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"); +// ot +var ot = require('./ot/index.js') -//public +// 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))); - } -} - -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 - } - }).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); - }); -} -//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); - } - 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; - } - }); -}, 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); - }); - }).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 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 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 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; - } - } - 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 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 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; + 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 { - return true; + next(new Error('AUTH failed: No cookie transmitted.')) } + } catch (ex) { + next(new Error('AUTH failed:' + JSON.stringify(ex))) + } } -var isConnectionBusy = false; -var connectionSocketQueue = []; -var isDisconnectBusy = false; -var disconnectSocketQueue = []; +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 finishConnection(socket, note, user) { - // if no valid info provided will drop the client - if (!socket || !note || !user) { - return interruptConnection(socket, note, user); +// 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) + } + 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) } - // 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); + }, 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 - 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)); - }); - } -} - -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" - }] - }]; - - 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 - }; - - return finishConnection(socket, notes[noteId], users[socket.id]); + 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) { - return failConnection(500, err, socket); - }); + logger.error(err) + return callback(err, null) + }) + } else { + return finishUpdateNote(note, _note, callback) + } } else { - return finishConnection(socket, notes[noteId], users[socket.id]); + note.lastchangeuserprofile = null + return finishUpdateNote(note, _note, callback) } + }).catch(function (err) { + logger.error(err) + return callback(err, null) + }) } -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 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 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]; +// 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 } - // 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]; - } + } + 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 +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 } - } - 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)); - }); - } + } + 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 user failed: ' + err) + }) + }).catch(function (err) { + return logger.error('count note failed: ' + err) + }) } -function buildUserOutData(user) { +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 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 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 isDuplicatedInSocketQueue (queue, socket) { + for (var i = 0; i < queue.length; i++) { + if (queue[i] && queue[i].id === socket.id) { + return true + } + } + 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 connectNextSocket () { + setTimeout(function () { + isConnectionBusy = false + if (connectionSocketQueue.length > 0) { + startConnection(connectionSocketQueue[0]) + } + }, 1) +} + +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 + } +} + +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 + + 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' + }] + }] + + 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 + } + + 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 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] + } + // 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 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 + } + } + 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) + }) + } + 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 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 + } + }) + if (found) { + color = randomcolor() + randomcount++ + } + } 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) + } + } + } + }).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) + } + } + }).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 = { - 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; + users: users } -} + socket.emit('online users', out) + }) -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; + // 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 } - //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); + 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) + }) } -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); - }); - } - 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 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); - } - //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); - }); - } - } - }); - - // 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); - }); - } - } - }); - - //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 585d1d5..31fa18b 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 -}; - -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 - }); + 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 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 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 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 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 newNote(req, res, next) { - var owner = null; - if (req.isAuthenticated()) { - owner = req.user.id; - } else if (!config.allowanonymous) { - return response.errorForbidden(res); +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 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) { + if (err) { + logger.log(err) } - models.Note.create({ - ownerId: owner, - alias: req.alias ? req.alias : null + models.Note.findOne({ + where: { + id: _id + }, + include: include || null }).then(function (note) { - return res.redirect(config.serverurl + "/" + LZString.compressToBase64(note.id)); + 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); - }); + 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 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)) } -} - -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 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 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 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 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, + 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 - }; - res.set({ - 'Access-Control-Allow-Origin': '*', //allow CORS as API + 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 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 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 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) + }) +} + +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) + } + } 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(data); + }) + res.send(out) + }) + } } -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); +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) } - 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) { +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 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, - 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); - } - } 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); - }); + client_id: config.github.clientID, + client_secret: config.github.clientSecret, + code: code, + state: state } -} - -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 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 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); - } + 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) } - }) - } -} - -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; + }) + } else { + return response.errorForbidden(res) } - }); + } else { + return response.errorForbidden(res) + } + }) + } } -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); - } - } - ); - }).catch(function (err) { - logger.error('gitlab action projects failed: ' + err); - return response.errorInternalError(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 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 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) + } + } + ) + }).catch(function (err) { + logger.error('gitlab action projects failed: ' + err) + return response.errorInternalError(res) + }) + } else { + return response.errorForbidden(res) + } } -function renderPublishSlide(data, res) { - res.set({ - 'Cache-Control': 'private' // only cache by client - }); - res.render(config.slidepath, data); +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) } -module.exports = response; +function renderPublishSlide (data, res) { + res.set({ + 'Cache-Control': 'private' // only cache by client + }) + res.render(config.slidepath, data) +} + +module.exports = response diff --git a/lib/workers/dmpWorker.js b/lib/workers/dmpWorker.js index 8e69636..6a1da98 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/locales/ru.json b/locales/ru.json index a1a95aa..f87f7c6 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -1,7 +1,7 @@ { "Collaborative markdown notes": "Совместные markdown заметки", "Realtime collaborative markdown notes on all platforms.": "Совместные markdown заметки в режиме реального времени на всех платформах.", - "Best way to write and share your knowledge in markdown.": "Лучший способ, чтобы записывать и делиться своими знаниями markdown.", + "Best way to write and share your knowledge in markdown.": "Лучший способ записывать свои знания и делиться ими в формате markdown.", "Intro": "Введение", "History": "История", "New guest note": "Новая гостевая заметка", @@ -101,4 +101,4 @@ "OR": "ИЛИ", "Export to Snippet": "Экспорт фрагмента кода", "Select Visibility Level": "Выберите уровень видимости" -} \ No newline at end of file +} diff --git a/package.json b/package.json index a179d93..0578932 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,8 @@ "main": "app.js", "license": "MIT", "scripts": { - "test": "npm run-script lint", - "lint": "eslint .", + "test": "npm run-script standard", + "standard": "node ./node_modules/standard/bin/cmd.js", "dev": "webpack --config webpack.config.js --progress --colors --watch", "build": "webpack --config webpack.production.js --progress --colors", "postinstall": "bin/heroku", @@ -152,7 +152,6 @@ "copy-webpack-plugin": "^4.0.1", "css-loader": "^0.26.1", "ejs-loader": "^0.3.0", - "eslint": "^3.15.0", "exports-loader": "^0.6.3", "expose-loader": "^0.7.1", "extract-text-webpack-plugin": "^1.0.1", @@ -165,8 +164,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 bc6e73f..a45a1c1 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: '
  • \ - \ - \ -
    \ -
    \ -
    \ -
    \ -

    \ -

    \ - visited \ -
    \ - \ - \ -

    \ -

    \ -
    \ -
    \ -
    \ -
  • ', - page: 18, - plugins: [ - ListPagination({ - outerWindow: 1 - }) - ] -}; -const historyList = new List('history', options); + valueNames: ['id', 'text', 'timestamp', 'fromNow', 'time', 'tags', 'pinned'], + item: '
  • ' + + '' + + '' + + '
    ' + + '
    ' + + '
    ' + + '
    ' + + '

    ' + + '

    ' + + ' visited ' + + '
    ' + + '' + + '' + + '

    ' + + '

    ' + + '
    ' + + '
    ' + + '
    ' + + '
  • ', + page: 18, + plugins: [ + window.ListPagination({ + outerWindow: 1 + }) + ] +} +const historyList = new List('history', options) -migrateHistoryFromTempCallback = pageInit; -setloginStateChangeEvent(pageInit); +window.migrateHistoryFromTempCallback = pageInit +setloginStateChangeEvent(pageInit) -pageInit(); +pageInit() -function pageInit() { - checkIfAuth( +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(); -}); + e.preventDefault() +}) -$(".ui-home").click(function (e) { - if (!$("#home").is(':visible')) { - $(".section:visible").hide(); - $("#home").fadeIn(); - } -}); +$('.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(); - } -}); +$('.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 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; - } - } - } - }); - // 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]); - } +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 } + } } - buildTagsFilter(filtertags); + }) + // 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]) } + } + } + } + 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(`${tags[j]}`); - } - 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(`${tags[j]}`) } + 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); -}); + } + $('.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(` ${value.text}
    ${value.time}`); - clearHistory = false; - deleteId = id; +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(` ${value.text}
    ${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; +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; - }); - }, () => { +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; - }); -} - -$(".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 => { - 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; - }); + } + $('.delete-modal').modal('hide') + deleteId = null + clearHistory = false + }) + }, () => { + if (clearHistory) { + saveHistory([]) + historyList.clear() + checkHistoryList() + deleteId = null } else { - historyList.filter(); + if (!deleteId) return + getHistory(notehistory => { + const newnotehistory = removeHistory(deleteId, notehistory) + saveHistory(newnotehistory) + historyList.remove('id', deleteId) + checkHistoryList() + deleteId = null + }) } - checkHistoryList(); -}); + $('.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(() => { + 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 + } + } +}) +$('.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() + } + checkHistoryList() +}) $('.search').keyup(() => { - checkHistoryList(); -}); + checkHistoryList() +}) diff --git a/public/js/extra.js b/public/js/extra.js index a3e840d..844d52c 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'); +/* eslint-env browser, jquery */ +/* global moment, serverurl */ -import Prism from 'prismjs'; -import hljs from 'highlight.js'; -import PDFObject from 'pdfobject'; -import S from 'string'; -import { saveAs } from 'file-saver'; +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') -require('./lib/common/login'); -require('../vendor/md-toc'); -var Viz = require("viz.js"); +import Prism from 'prismjs' +import hljs from 'highlight.js' +import PDFObject from 'pdfobject' +import S from 'string' +import { saveAs } from 'file-saver' -//auto update last change -window.createtime = null; -window.lastchangetime = null; +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')); - } -} -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(); - } - } -} - -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(); - } - } -} - -//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; +export function updateLastChange () { + if (!window.lastchangeui) return + if (window.createtime) { + if (window.createtime && !window.lastchangetime) { + window.lastchangeui.status.text('created') } else { - const h1s = view.find("h1"); - if (h1s.length > 0) { - title = h1s.first().text(); - } else { - title = null; - } + window.lastchangeui.status.text('changed') } - return title; + 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) -//render title -export function renderTitle(view) { - let title = getTitle(view); - if (title) { - title += ' - HackMD'; +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 { - title = 'HackMD - Collaborative markdown notes'; + window.lastchangeui.user.hide() + window.lastchangeui.nouser.show() } - return title; + } } -//render filename -export function renderFilename(view) { - let filename = getTitle(view); - if (!filename) { - filename = 'Untitled'; +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() } - return filename; + } +} + +// 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 { + title = null + } + } + 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 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; +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"); +// 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, ''); - html = html.replace(nameandtimeregex, ' $1 $2'); - html = html.replace(nameregex, ' $1'); - html = html.replace(timeregex, ' $1'); - return html; +function replaceExtraTags (html) { + html = html.replace(coloregex, '') + html = html.replace(nameandtimeregex, ' $1 $2') + html = html.replace(nameregex, ' $1') + html = html.replace(timeregex, ' $1') + 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*/, ``) - .replace(/^\s*\[x\]\s*/, ``); - 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] } + 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*/, ``) + .replace(/^\s*\[x\]\s*/, ``) + 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") + // 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 = ``; - $(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('
    ' + err + '
    '); - 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('
    ' + err + '
    '); - 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('
    ' + err + '
    '); - 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 = `` + $(value).prepend(image) + if (window.viewAjaxCallback) window.viewAjaxCallback() } - } catch (err) { - $value.unwrap(); - $value.parent().append('
    ' + err + '
    '); - 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") - .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 = $('
    ').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") - .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 = $('
    ').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") - .each(function (key, value) { - const url = $(value).attr('data-pdfurl'); - const inner = $('
    '); - $(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); - } - }); - //mathjax - const mathjaxdivs = view.find('span.mathjax.raw').removeClass("raw").toArray(); + }) + }) + // 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 { - 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); - } + 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) { - console.warn(err); + $value.unwrap() + $value.parent().append('
    ' + err + '
    ') + console.warn(err) } - //render title - document.title = renderTitle(view); -} + }) + // flowchart + const flow = view.find('div.flow-chart.raw').removeClass('raw') + flow.each((key, value) => { + try { + var $value = $(value) + const $ele = $(value).parent().parent() -//only static transform should be here -export function postProcess(code) { - const result = $(`
    ${code}
    `); - //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 = $('
    ' + md.metaError + '
    ') - result.prepend(warning); - } + 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('
    ' + err + '
    ') + console.warn(err) } - return result; -} -window.postProcess = postProcess; + }) + // graphviz + var graphvizs = view.find('div.graphviz.raw').removeClass('raw') + graphvizs.each(function (key, value) { + try { + var $value = $(value) + var $ele = $(value).parent().parent() -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.attr('src', url + id); - iframe.attr('style', style); - $(value).html(iframe); - } - }); - return src; -} + var graphviz = Viz($value.text()) + if (!graphviz) throw Error('viz.js output empty graph') + $value.html(graphviz) -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); -} + $ele.addClass('graphviz') + $value.children().unwrap().unwrap() + } catch (err) { + $value.unwrap() + $value.parent().append('
    ' + err + '
    ') + 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') -//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); - // console.log(html); - const blob = new Blob([html], { - type: "text/html;charset=utf-8" - }); - saveAs(blob, filename, true); - }); - }); -} + let mermaidError = null + window.mermaid.parseError = (err, hash) => { + mermaidError = err + } -//jQuery sortByDepth -$.fn.sortByDepth = function () { - const ar = this.map(function () { - return { - length: $(this).parents().length, - elt: this + 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('
    ' + err + '
    ') + 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 (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 = $('
    ').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() } - }).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'); + }) + }) + // 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 = $('
    ').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 = $('
    ') + $(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() + 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) + } + } + 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) } + } catch (err) { + console.warn(err) + } + // render title + document.title = renderTitle(view) } -//remove hash -function removeHash() { - history.pushState("", document.title, window.location.pathname + window.location.search); -} - -let tocExpand = false; - -function checkExpandToggle() { - const toc = $('.ui-toc-dropdown .toc'); - const toggle = $('.expand-toggle'); - if (!tocExpand) { - toc.removeClass('expand'); - toggle.text('Expand all'); +// only static transform should be here +export function postProcess (code) { + const result = $(`
    ${code}
    `) + // 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 { - toc.addClass('expand'); - toggle.text('Collapse all'); + warning = $('
    ' + md.metaError + '
    ') + result.prepend(warning) } + } + 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.attr('src', url + id) + iframe.attr('style', style) + $(value).html(iframe) + } + }) + return src } -//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 = $('
    Expand all'); - const backtotop = $('Back to top'); - const gotobottom = $('Go to bottom'); - 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); +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) } -//smooth all hash trigger scrolling -export function smoothHashScroll() { - const hashElements = $("a[href^='#']:not([smoothhashscroll])").toArray(); +// 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) + }) + }) +} - for (const element of hashElements) { - const $element = $(element); - const hash = element.hash; - if (hash) { - $element.on('click', function (e) { +// 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 = 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') + } +} + +// remove hash +function removeHash () { + history.pushState('', document.title, window.location.pathname + window.location.search) +} + +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('') + /* 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 = $('
    Expand all') + const backtotop = $('Back to top') + const gotobottom = $('Go to bottom') + 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() + + 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).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).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 = ""; - anchor.title = id; - return anchor; -}; + const anchor = document.createElement('a') + anchor.className = 'anchor hidden-xs' + anchor.href = `#${id}` + anchor.innerHTML = '' + anchor.title = id + return anchor +} const linkifyAnchors = (level, containingElement) => { - const headers = containingElement.getElementsByTagName(`h${level}`); + 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); + 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) } + } } -function getHeaderContent(header) { - const headerHTML = $(header).clone(); - headerHTML.find('.MathJax_Preview').remove(); - headerHTML.find('.MathJax').remove(); - return headerHTML[0].innerHTML; +export function autoLinkify (view) { + const contentBlock = view[0] + if (!contentBlock) { + return + } + for (let level = 1; level <= 6; level++) { + linkifyAnchors(level, contentBlock) + } } -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); - } - } +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 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 +} + +function highlightRender (code, lang) { + if (!lang || /no(-?)highlight|plain|text/.test(lang)) { return } + code = S(code).escapeHTML().s + if (lang === 'sequence') { + return `
    ${code}
    ` + } else if (lang === 'flow') { + return `
    ${code}
    ` + } else if (lang === 'graphviz') { + return `
    ${code}
    ` + } else if (lang === 'mermaid') { + return `
    ${code}
    ` + } + 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] = `` } + const continuelinenumber = /=\+$/.test(lang) + const linegutter = `
    ${linenumbers.join('\n')}
    ` + result.value = `
    ${linegutter}
    ${result.value}
    ` + } + return result.value } -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 `
    ${code}
    `; - } else if (lang == 'flow') { - return `
    ${code}
    `; - } else if (lang == 'graphviz') { - return `
    ${code}
    `; - } else if (lang == 'mermaid') { - return `
    ${code}
    `; - } - 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] = ``; - } - const continuelinenumber = /\=\+$/.test(lang); - const linegutter = `
    ${linenumbers.join('\n')}
    `; - result.value = `
    ${linegutter}
    ${result.value}
    `; - } - 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; + 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-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: '', - afterMath: '', - beforeInlineMath: '\\(', - afterInlineMath: '\\)', - beforeDisplayMath: '\\[', - afterDisplayMath: '\\]' -})); -md.use(require('markdown-it-imsize')); + beforeMath: '', + afterMath: '', + beforeInlineMath: '\\(', + afterInlineMath: '\\)', + beforeDisplayMath: '\\[', + afterDisplayMath: '\\]' +})) +md.use(require('markdown-it-imsize')) md.use(require('markdown-it-emoji'), { - shortcuts: {} -}); + 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 -}); +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) => emojify.replace(`:${token[idx].markup}:`); +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); +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; + 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 (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 (options.highlight) { + highlighted = options.highlight(token.content, langName) || md.utils.escapeHtml(token.content) + } else { + highlighted = md.utils.escapeHtml(token.content) + } - if (highlighted.indexOf('${highlighted}\n`; -}; + return `
    ${highlighted}
    \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.attr('data-videoid', videoid); - const thumbnail_src = `//img.youtube.com/vi/${videoid}/hqdefault.jpg`; - const image = ``; - div.append(image); - const icon = ''; - div.append(icon); - return div[0].outerHTML; + const videoid = match[1] + if (!videoid) return + const div = $('
    ') + div.attr('data-videoid', videoid) + const thumbnailSrc = `//img.youtube.com/vi/${videoid}/hqdefault.jpg` + const image = `` + div.append(image) + const icon = '' + 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.attr('data-videoid', videoid); - const icon = ''; - div.append(icon); - return div[0].outerHTML; + const videoid = match[1] + if (!videoid) return + const div = $('
    ') + div.attr('data-videoid', videoid) + const icon = '' + 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 = ``; - return code; + const gistid = match[1] + const code = `` + return code } -); -//TOC +) +// TOC const tocPlugin = new Plugin( // regexp to match /^\[TOC\]$/i, (match, utils) => '
    ' -); -//slideshare +) +// slideshare const slidesharePlugin = new Plugin( // regexp to match /{%slideshare\s*([\d\D]*?)\s*%}/, (match, utils) => { - const slideshareid = match[1]; - const div = $('
    '); - div.attr('data-slideshareid', slideshareid); - return div[0].outerHTML; + const slideshareid = match[1] + const 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.attr('data-speakerdeckid', speakerdeckid); - return div[0].outerHTML; + const speakerdeckid = match[1] + const 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.attr('data-pdfurl', pdfurl); - return div[0].outerHTML; + const pdfurl = match[1] + if (!isValidURL(pdfurl)) return match[0] + const 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; +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); - } + 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; + 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; - } + 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; + state.line = line + 1 - return true; + 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 94aa77f..5006cd2 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 eabc5b7..6c0e8a6 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.url = options.url; - if (!this.url) { - var params = options.params || {}; - params.uploadType = 'resumable'; - this.url = this.buildUrl_(options.fileId, params, options.baseUrl); } - this.httpMethod = options.fileId ? 'PUT' : 'POST'; -}; + 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 + if (!this.url) { + var params = options.params || {} + params.uploadType = 'resumable' + this.url = this.buildUrl_(options.fileId, params, options.baseUrl) + } + 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 34b2cba..e14b80d 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) { +function saveHistoryToServer (notehistory) { + $.post(`${serverurl}/history`, { + history: JSON.stringify(notehistory) + }) +} + +export function saveStorageHistoryToServer (callback) { + const data = store.get('notehistory') + if (data) { $.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 - }) + history: data + }) .done(data => { - callback(data); - }); - } + callback(data) + }) + } } -function saveCookieHistoryToServer(callback) { - $.post(`${serverurl}/history`, { - history: Cookies.get('notehistory') - }) - .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; +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 +} + +// used for outer +export function getHistory (callback) { + checkIfAuth( + () => { + getServerHistory(callback) + }, + () => { + getStorageHistory(callback) + } + ) +} + +function getServerHistory (callback) { + $.get(`${serverurl}/history`) + .done(data => { + if (data.history) { + callback(data.history) + } + }) + .fail((xhr, status, error) => { + console.error(xhr.responseText) + }) +} + +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 parseHistory (list, callback) { + checkIfAuth( + () => { + parseServerToHistory(list, callback) + }, + () => { + parseStorageToHistory(list, callback) + } + ) +} + +export function parseServerToHistory (list, callback) { + $.get(`${serverurl}/history`) + .done(data => { + if (data.history) { + parseToHistory(list, data.history, callback) + } + }) + .fail((xhr, status, error) => { + console.error(xhr.responseText) + }) +} + +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) + } +} + +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++) { - 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; -} - -//used for outer -export function getHistory(callback) { - checkIfAuth( - () => { - getServerHistory(callback); - }, - () => { - getStorageHistory(callback); - } - ); -} - -function getServerHistory(callback) { - $.get(`${serverurl}/history`) - .done(data => { - if (data.history) { - callback(data.history); - } - }) - .fail((xhr, status, error) => { - console.error(xhr.responseText); - }); -} - -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 parseHistory(list, callback) { - checkIfAuth( - () => { - parseServerToHistory(list, callback); - }, - () => { - parseStorageToHistory(list, callback); - } - ); -} - -export function parseServerToHistory(list, callback) { - $.get(`${serverurl}/history`) - .done(data => { - if (data.history) { - parseToHistory(list, data.history, callback); - } - }) - .fail((xhr, status, error) => { - console.error(xhr.responseText); - }); -} - -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); - } -} - -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'); + // 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); - }); -} - -export function deleteServerHistory(noteId, callback) { - $.ajax({ - url: `${serverurl}/history${noteId ? '/' + noteId : ""}`, - type: 'DELETE' + console.error(xhr.responseText) + return callback(error, null) }) +} + +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 1c2c5eb..1a873ac 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 f0c476e..e672a68 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; + 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; +// 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" -}; + '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!"); +// 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')); + 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 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; +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(); - }); +function getStatusBarTemplate (callback) { + $.get(serverurl + '/views/statusbar.html', function (template) { + statusBarTemplate = template + if (callback) callback() + }) } -getStatusBarTemplate(); +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" - }); +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(); + 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); - } +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); + } + 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'); + 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"; - } - 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(); -} - -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'); - } - } - 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); - } -} - -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(); + function setType () { + if (editor.getOption('indentWithTabs')) { + Cookies.set('indent_type', 'tab', { + expires: 365 + }) + type.text('Tab Size:') } else { - Cookies.remove('preferences-override-browser-keymap'); - resetEditorKeymapToBrowserKeymap(); + Cookies.set('indent_type', 'space', { + expires: 365 + }) + type.text('Spaces:') } -} + } + setType() -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); + function setUnit () { + var unit = editor.getOption('indentUnit') + if (editor.getOption('indentWithTabs')) { + Cookies.set('tab_size', unit, { + expires: 365 + }) } else { - overrideBrowserKeymap.prop('checked', false); + Cookies.set('space_units', unit, { + expires: 365 + }) } - setOverrideBrowserKeymap(); + widthLabel.text(unit) + } + setUnit() - overrideBrowserKeymap.change(function() { - setOverrideBrowserKeymap(); - }); + 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') + }) } -var selection = null; +function setKeymap () { + var cookieKeymap = Cookies.get('keymap') + if (cookieKeymap) { editor.setOption('keyMap', cookieKeymap) } -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); + 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') + } + } + 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') + } + } + 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.'); - } + 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 +// 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") + 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 +// 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]); + 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 +} -//idle +/* 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); + var hidden = Visibility.hidden() + if (hidden) { + if (editorHasFocus()) { + wasFocus = true + editor.getInputField().blur() } - updateTitleReminder(); -}); + } 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(); -}); + // setNeedRefresh(); +}) -setupSyncAreas(ui.area.codemirrorScroll, ui.area.view, ui.area.markdown); +setupSyncAreas(ui.area.codemirrorScroll, ui.area.view, ui.area.markdown) -function autoSyncscroll() { - if (editorHasFocus()) { - syncScrollToView(); - } else { - syncScrollToEdit(); - } +function autoSyncscroll () { + if (editorHasFocus()) { + syncScrollToView() + } else { + syncScrollToEdit() + } } -var windowResizeDebounce = 200; -var windowResize = _.debounce(windowResizeInner, windowResizeDebounce); +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 { +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 { // 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 = $(''); - 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); - }); - } + window.preventSyncScrollToView = 2 + window.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 = $('') + 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 (syncscroll) { - if (previousFocusOnEditor) { - preventSyncScrollToView = false; - syncScrollToView(); - } else { - preventSyncScrollToEdit = false; - syncScrollToEdit(); - } - ui.area.resize.syncToggle.find('i').removeClass('fa-unlink').addClass('fa-link'); +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); + 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 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) { - currentStatus = type; - var shortStatus = ui.toolbar.shortStatus; - var status = ui.toolbar.status; - var label = $(''); - var fa = $(''); - var msg = ""; - var shortMsg = ""; +function showStatus (type, num) { + window.currentStatus = type + var shortStatus = ui.toolbar.shortStatus + var status = ui.toolbar.status + var label = $('') + var fa = $('') + var msg = '' + var shortMsg = '' - shortStatus.html(""); - status.html(""); + 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; - } + 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(); + label.append(fa) + var shortLabel = label.clone() - shortLabel.append(" " + shortMsg); - shortStatus.append(shortLabel); + shortLabel.append(' ' + shortMsg) + shortStatus.append(shortLabel) - label.append(" " + msg); - status.append(label); + 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; - } +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; +var lastMode = null -function changeMode(type) { +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(); + 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(); + windowResizeInner() - restoreInfo(); + restoreInfo() - if (lastMode == modeType.view && currentMode == modeType.both) { - preventSyncScrollToView = 2; - syncScrollToEdit(null, true); - } + if (lastMode === modeType.view && window.currentMode === modeType.both) { + window.preventSyncScrollToView = 2 + syncScrollToEdit(null, true) + } - if (lastMode == modeType.edit && currentMode == modeType.both) { - preventSyncScrollToEdit = 2; - syncScrollToView(null, true); - } + if (lastMode === modeType.edit && window.currentMode === modeType.both) { + window.preventSyncScrollToEdit = 2 + syncScrollToView(null, true) + } - if (lastMode == modeType.both && currentMode != modeType.both) { - preventSyncScrollToView = false; - preventSyncScrollToEdit = false; - } + if (lastMode === modeType.both && window.currentMode !== modeType.both) { + window.preventSyncScrollToView = false + window.preventSyncScrollToEdit = false + } - if (lastMode != modeType.edit && currentMode == modeType.edit) { - editor.refresh(); - } + if (lastMode !== modeType.edit && window.currentMode === modeType.edit) { + editor.refresh() + } - $(document.body).scrollspy('refresh'); - ui.area.view.scrollspy('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(); + 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'); +function lockNavbar () { + $('.navbar').addClass('locked') } var unlockNavbar = _.debounce(function () { - $('.navbar').removeClass('locked'); -}, 200); + $('.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 -} - -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) { - $('' ); - - 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] + ''; - } - - // prevent script end tags in the content from interfering - // with parsing - content = content.replace( /<\/script>/g, SCRIPT_END_PLACEHOLDER ); - - return ''; - - } - - /** - * 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
    tags - for( var i = 0, len = sectionStack.length; i < len; i++ ) { - // vertical - if( sectionStack[i] instanceof Array ) { - markdownSections += '
    '; - - sectionStack[i].forEach( function( child ) { - markdownSections += '
    ' + createMarkdownSlide( child, options ) + '
    '; - } ); - - markdownSections += '
    '; - } - else { - markdownSections += '
    ' + createMarkdownSlide( sectionStack[i], options ) + '
    '; - } - } - - 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 = '
    ' + - 'ERROR: The attempt to fetch ' + url + ' failed with HTTP status ' + xhr.status + '.' + - 'Check your browser\'s JavaScript console for more details.' + - '

    Remember that you need to serve the presentation HTML from a HTTP server.

    ' + - '
    '; - - } - } - }; - - 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'), '') + + 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] + '' + } + + // prevent script end tags in the content from interfering + // with parsing + content = content.replace(/<\/script>/g, SCRIPT_END_PLACEHOLDER) + + return '' + } + + /** + * 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
    tags + for (var i = 0, len = sectionStack.length; i < len; i++) { + // vertical + if (sectionStack[i] instanceof Array) { + markdownSections += '
    ' + + sectionStack[i].forEach(function (child) { + markdownSections += '
    ' + createMarkdownSlide(child, options) + '
    ' + }) + + markdownSections += '
    ' + } else { + markdownSections += '
    ' + createMarkdownSlide(sectionStack[i], options) + '
    ' + } + } + + 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 = '
    ' + + 'ERROR: The attempt to fetch ' + url + ' failed with HTTP status ' + xhr.status + '.' + + 'Check your browser\'s JavaScript console for more details.' + + '

    Remember that you need to serve the presentation HTML from a HTTP server.

    ' + + '
    ' + } + } + } + + 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 63cf64c..e743bb5 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(); -}); + // tooltip + $('[data-toggle="tooltip"]').tooltip() +}) -function extend() { - const target = {}; +function extend () { + const target = {} - for (const source of arguments) { - for (const key in source) { - if (source.hasOwnProperty(key)) { - target[key] = source[key]; - } - } + 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(); -}; + 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(); - } +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 c969317..c227f83 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; + 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 (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 (options.highlight) { + highlighted = options.highlight(token.content, langName) || md.utils.escapeHtml(token.content) + } else { + highlighted = md.utils.escapeHtml(token.content) + } - if (highlighted.indexOf('${highlighted}
    \n`; - } + if (tokens[idx].map && tokens[idx].level === 0) { + const startline = tokens[idx].map[0] + 1 + const endline = tokens[idx].map[1] + return `
    ${highlighted}
    \n` + } - return `
    ${highlighted}
    \n`; -}; + return `
    ${highlighted}
    \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 `
    ${md.utils.escapeHtml(tokens[idx].content)}
    \n`; - } - return `
    ${md.utils.escapeHtml(tokens[idx].content)}
    \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 `
    ${md.utils.escapeHtml(tokens[idx].content)}
    \n` + } + return `
    ${md.utils.escapeHtml(tokens[idx].content)}
    \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; +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(); + 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]; + 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); + _lineHeightMap.push(acc) - if (str.length === 0) { - acc++; - continue; - } - - const h = editor.heightAtLine(i + 1) - editor.heightAtLine(i); - acc += Math.round(h / lineHeight); - } - _lineHeightMap.push(acc); - linesCount = acc; - - for (i = 0; i < linesCount; i++) { - _scrollMap.push(-1); + if (str.length === 0) { + acc++ + continue } - nonEmptyList.push(0); + 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) // make the first line go top - _scrollMap[0] = viewTop; + _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); + 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 = _scrollMap + lineHeightMap = _lineHeightMap - _scrollMap[0] = 0; - - scrollMap = _scrollMap; - lineHeightMap = _lineHeightMap; - - if (loaded && callback) callback(); + if (window.loaded && callback) callback() } // sync view scroll progress to edit -let viewScrollingTimer = null; +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 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 } + return + } + if (!scrollMap || !lineHeightMap) { + buildMap(() => { + syncScrollToEdit(event, preventAnimate) + }) + return + } + if (editScrolling) return - if (preventAnimate) { - editArea.scrollTop(posTo); + 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 } + } + 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 + } + } - viewScrolling = true; - clearTimeout(viewScrollingTimer); - viewScrollingTimer = setTimeout(viewScrollingTimeoutInner, duration * 1.5); + 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; +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; +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 { + window.preventSyncScrollToView = false } - if (!scrollMap || !lineHeightMap) { - buildMap(() => { - syncScrollToView(event, preventAnimate); - }); - return; - } - if (viewScrolling) return; + 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); + 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); - } + 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"); - } + 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); + 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 200275a..f93f792 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._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._init = function () { - this._collectTitleElements(); - this._createTocContent(); - this._showToc(); - }; + this._elTitlesNames.length = this._elTitlesNames.length > this.tocLevel ? this.tocLevel : this._elTitlesNames.length - 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); + 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 = [] + + 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 += '
  • ' + this._elTitleElementText + '' + + 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 } - } - - 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]); - } - } - }; - - 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; - } - - this.tocContent += '
  • ' + this._elTitleElementText + ''; - - 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('
  • '); - this._tempLists.length = this._tempLists.length - y; - } else { - this._tempLists.push(this._elTitleElement); - if (this.ulClass) - this.tocContent += '
      '; - else - this.tocContent += '
        '; - } - } else { - this.tocContent += ''; - } - } else { - if (this._tempLists.length) { - this.tocContent += new Array(this._tempLists.length + 1).join('
      '); - } else { - this.tocContent += ''; - } - } - } - if (this.ulClass) - this.tocContent = '
        ' + this.tocContent + '
      '; - else - this.tocContent = '
        ' + this.tocContent + '
      '; - }; - - 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); + y++ + } + if (checkColse) { + this.tocContent += new Array(y + 1).join('
    ') + this._tempLists.length = this._tempLists.length - y + } else { + this._tempLists.push(this._elTitleElement) + if (this.ulClass) { this.tocContent += '
      ' } else { this.tocContent += '
        ' } + } } else { - document.getElementById(this.options.targetId).appendChild(this.toc); + this.tocContent += '' } - 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;'); - } - } + } else { + if (this._tempLists.length) { + this.tocContent += new Array(this._tempLists.length + 1).join('
      ') + } else { + this.tocContent += '' } - }; - window.Toc = Toc; -})(window); \ No newline at end of file + } + } + if (this.ulClass) { this.tocContent = '
        ' + this.tocContent + '
      ' } else { this.tocContent = '
        ' + this.tocContent + '
      ' } + } + + 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) diff --git a/webpack.config.js b/webpack.config.js index 236490b..f9f0a1c 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 7c690d2..7b42843 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 419149c..9ab4c06 100644 --- a/webpackBaseConfig.js +++ b/webpackBaseConfig.js @@ -1,423 +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: '**/*', - dot: false - }, - to: 'emojify.js/' - }, - { - context: path.join(__dirname, 'node_modules/reveal.js'), - from: { - glob: '**/*', - dot: false - }, - to: 'reveal.js/' - } - ]) + 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 %>' + }, - 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') - } - }, - - externals: { - "viz.js": "Viz", - "socket.io-client": "io", - "lodash": "_", - "jquery": "$", - "moment": "moment", - "handlebars": "Handlebars", - "highlight.js": "hljs", - "select2": "select2" - }, - - 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" + 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') } -}; + }, + + externals: { + 'viz.js': 'Viz', + 'socket.io-client': 'io', + 'lodash': '_', + 'jquery': '$', + 'moment': 'moment', + 'handlebars': 'Handlebars', + 'highlight.js': 'hljs', + 'select2': 'select2' + }, + + 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 + } +}