Jump to 0.3.1

This commit is contained in:
Wu Cheng-Han 2015-07-02 00:10:20 +08:00
parent f7f8c901f4
commit 10c9811fc5
49 changed files with 2336 additions and 448 deletions

View file

@ -1,52 +1,70 @@
HackMD 0.2.9
HackMD 0.3.1
===
HackMD is a realtime collaborative markdown notes on all platforms.
Inspired by Hackpad, but more focusing on speed and flexibility.
Still in early stage, feel free to fork or contribute to this.
HackMD is a realtime collaborative markdown notes on all platforms.
Inspired by Hackpad, but more focusing on speed and flexibility.
Still in early stage, feel free to fork or contribute to this.
Thanks for your using! :smile:
Dependency
Database dependency
---
- PostgreSQL 9.3.6 or 9.4.1
- MongoDB 3.0.2
Import db schema
Import database schema
---
The notes are store in PostgreSQL, the schema is in the `hackmd_schema.sql`
The notes are store in PostgreSQL, the schema is in the `hackmd_schema.sql`
To import the sql file in PostgreSQL, type `psql -i hackmd_schema.sql`
The users, temps and sessions are store in MongoDB, which don't need schema, so just make sure you have the correct connection string.
The users, temps and sessions are store in MongoDB, which don't need schema, so just make sure you have the correct connection string.
Config
Structure
---
```
hackmd/
├── logs/ --- server logs
├── backups/ --- db backups
├── tmp/ --- temporary files
├── lib/ --- server libraries
└── public/ --- client files
├── css/ --- css styles
├── js/ --- js scripts
├── vendor/ --- vendor includes
└── views/ --- view templates
```
Configure
---
There are some config you need to change in below files
```
./run.sh
./config.js
./public/js/common.js
./Procfile --- for heroku start
./run.sh --- for forever start
./processes.json --- for pm2 start
./config.js --- for server settings
./public/js/common.js --- for client settings
./hackmd --- for logrotate
```
The script `run.sh`, it's for someone like me to run the server via npm package `forever`, and can passing environment variable to the server, like heroku does.
**From 0.3.1, we no longer recommend using `forever` to run your server.**
To install `forever`, just type `npm install forever -g`
We using `pm2` to run server.
See [here](https://github.com/Unitech/pm2) for details.
You can use SSL to encrypt your site by passing certificate path in the `config.js` and set `usessl=true`
Run a server
---
To run the server, type `bash run.sh`
Log will be at `~/.forever/hackmd.log`
- forever: `bash run.sh`
- pm2: `pm2 start processes.json`
Stop a server
---
To stop the server, simply type `forever stop hackmd`
- forever: `forever stop hackmd`
- pm2: `pm2 stop hackmd`
Backup db
---
To backup the db, type `bash backup.sh`
Backup files will be at `./backups/`
To backup the db, type `bash backup.sh`
**License under MIT.**

67
app.js
View file

@ -49,13 +49,15 @@ if (config.usessl) {
var app = express();
var server = require('http').createServer(app);
}
//socket io listen
var io = require('socket.io').listen(server);
//logger
app.use(morgan('combined', {
"stream": logger.stream
}));
//socket io
var io = require('socket.io')(server);
// connect to the mongodb
mongoose.connect(process.env.MONGOLAB_URI || config.mongodbstring);
@ -80,7 +82,7 @@ var sessionStore = new MongoStore({
touchAfter: config.sessiontouch
},
function (err) {
console.log(err);
logger.info(err);
});
//compression
@ -115,12 +117,12 @@ app.use(passport.session());
//serialize and deserialize
passport.serializeUser(function (user, done) {
//console.log('serializeUser: ' + user._id);
//logger.info('serializeUser: ' + user._id);
done(null, user._id);
});
passport.deserializeUser(function (id, done) {
User.model.findById(id, function (err, user) {
//console.log(user)
//logger.info(user)
if (!err) done(null, user);
else done(err, null);
})
@ -163,7 +165,7 @@ app.get("/temp", function (req, res) {
});
temp.remove(function (err) {
if (err)
console.log('remove temp failed: ' + err);
logger.error('remove temp failed: ' + err);
});
}
});
@ -182,7 +184,7 @@ app.post("/temp", urlencodedParser, function (req, res) {
response.errorForbidden(res);
else {
if (config.debug)
console.log('SERVER received temp from [' + host + ']: ' + req.body.data);
logger.info('SERVER received temp from [' + host + ']: ' + req.body.data);
Temp.newTemp(id, data, function (err, temp) {
if (!err && temp) {
res.header("Access-Control-Allow-Origin", "*");
@ -247,7 +249,7 @@ app.get('/auth/dropbox/callback',
//logout
app.get('/logout', function (req, res) {
if (config.debug && req.session.passport.user)
console.log('user logout: ' + req.session.passport.user);
logger.info('user logout: ' + req.session.passport.user);
req.logout();
res.redirect('/');
});
@ -256,7 +258,7 @@ app.get('/history', function (req, res) {
if (req.isAuthenticated()) {
User.model.findById(req.session.passport.user, function (err, user) {
if (err) {
console.log('read history failed: ' + err);
logger.error('read history failed: ' + err);
} else {
var history = [];
if (user.history)
@ -274,18 +276,18 @@ app.get('/history', function (req, res) {
app.post('/history', urlencodedParser, function (req, res) {
if (req.isAuthenticated()) {
if (config.debug)
console.log('SERVER received history from [' + req.session.passport.user + ']: ' + req.body.history);
logger.info('SERVER received history from [' + req.session.passport.user + ']: ' + req.body.history);
User.model.findById(req.session.passport.user, function (err, user) {
if (err) {
console.log('write history failed: ' + err);
logger.error('write history failed: ' + err);
} else {
user.history = req.body.history;
user.save(function (err) {
if (err) {
console.log('write user history failed: ' + err);
logger.error('write user history failed: ' + err);
} else {
if (config.debug)
console.log("write user history success: " + user._id);
logger.info("write user history success: " + user._id);
};
});
}
@ -300,7 +302,7 @@ app.get('/me', function (req, res) {
if (req.isAuthenticated()) {
User.model.findById(req.session.passport.user, function (err, user) {
if (err) {
console.log('read me failed: ' + err);
logger.error('read me failed: ' + err);
} else {
var profile = JSON.parse(user.profile);
res.send({
@ -324,20 +326,25 @@ app.post('/uploadimage', function (req, res) {
response.errorForbidden(res);
} else {
if (config.debug)
console.log('SERVER received uploadimage: ' + JSON.stringify(files.image));
logger.info('SERVER received uploadimage: ' + JSON.stringify(files.image));
imgur.setClientId(config.imgur.clientID);
imgur.uploadFile(files.image.path)
.then(function (json) {
if (config.debug)
console.log('SERVER uploadimage success: ' + JSON.stringify(json));
res.send({
link: json.data.link
try {
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
});
})
.catch(function (err) {
logger.error(err);
res.send('upload image error');
});
})
.catch(function (err) {
console.error(err);
res.send(err.message);
});
} catch (err) {
logger.error(err);
res.send('upload image error');
}
}
});
});
@ -345,6 +352,10 @@ app.post('/uploadimage', function (req, res) {
app.get("/new", response.newNote);
//get features
app.get("/features", response.showFeatures);
//get share note
app.get("/s/:shortid", response.showShareNote);
//share note actions
app.get("/s/:shortid/:action", response.shareNoteActions);
//get note by id
app.get("/:noteId", response.showNote);
//note actions
@ -370,10 +381,10 @@ io.sockets.on('connection', realtime.connection);
//listen
if (config.usessl) {
server.listen(config.sslport, function () {
console.log('HTTPS Server listening at sslport %d', config.sslport);
logger.info('HTTPS Server listening at sslport %d', config.sslport);
});
} else {
server.listen(config.port, function () {
console.log('HTTP Server listening at port %d', config.port);
logger.info('HTTP Server listening at port %d', config.port);
});
}

0
backups/.keep Normal file
View file

View file

@ -11,7 +11,7 @@ var urladdport = true; //add port on getserverurl
var config = {
debug: true,
version: '0.2.8',
version: '0.3.1',
domain: domain,
alloworigin: ['add here to allow origin to cross'],
testport: testport,

11
hackmd Normal file
View file

@ -0,0 +1,11 @@
/home/hackmd/logs/*.log {
daily
rotate 365
missingok
notifempty
compress
sharedscripts
copytruncate
dateext
dateformat %Y-%m-%d
}

View file

@ -7,17 +7,18 @@ var GithubStrategy = require('passport-github').Strategy;
var DropboxStrategy = require('passport-dropbox-oauth2').Strategy;
//core
var User = require('./user.js')
var config = require('../config.js')
var User = require('./user.js');
var config = require('../config.js');
var logger = require("./logger.js");
function callback(accessToken, refreshToken, profile, done) {
//console.log(profile.displayName || profile.username);
//logger.info(profile.displayName || profile.username);
User.findOrNewUser(profile.id, profile, function (err, user) {
if (err || user == null) {
console.log('auth callback failed: ' + err);
logger.error('auth callback failed: ' + err);
} else {
if(config.debug && user)
console.log('user login: ' + user._id);
if (config.debug && user)
logger.info('user login: ' + user._id);
done(null, user);
}
});

View file

@ -6,6 +6,7 @@ var util = require('util');
//core
var config = require("../config.js");
var logger = require("./logger.js");
//public
var db = {
@ -48,18 +49,18 @@ function newToDB(id, owner, body, callback) {
if (err) {
client.end();
callback(err, null);
return console.error('could not connect to postgres', err);
return logger.error('could not connect to postgres', err);
}
var newnotequery = util.format(insertquery, id, owner, body);
//console.log(newnotequery);
//logger.info(newnotequery);
client.query(newnotequery, function (err, result) {
client.end();
if (err) {
callback(err, null);
return console.error("new note to db failed: " + err);
return logger.error("new note to db failed: " + err);
} else {
if (config.debug)
console.log("new note to db success");
logger.info("new note to db success");
callback(null, result);
}
});
@ -72,22 +73,22 @@ function readFromDB(id, callback) {
if (err) {
client.end();
callback(err, null);
return console.error('could not connect to postgres', err);
return logger.error('could not connect to postgres', err);
}
var readquery = util.format(selectquery, id);
//console.log(readquery);
//logger.info(readquery);
client.query(readquery, function (err, result) {
client.end();
if (err) {
callback(err, null);
return console.error("read from db failed: " + err);
return logger.error("read from db failed: " + err);
} else {
//console.log(result.rows);
//logger.info(result.rows);
if (result.rows.length <= 0) {
callback("not found note in db: " + id, null);
} else {
if(config.debug)
console.log("read from db success");
logger.info("read from db success");
callback(null, result);
}
}
@ -101,18 +102,18 @@ function saveToDB(id, title, data, callback) {
if (err) {
client.end();
callback(err, null);
return console.error('could not connect to postgres', err);
return logger.error('could not connect to postgres', err);
}
var savequery = util.format(updatequery, title, data, id);
//console.log(savequery);
//logger.info(savequery);
client.query(savequery, function (err, result) {
client.end();
if (err) {
callback(err, null);
return console.error("save to db failed: " + err);
return logger.error("save to db failed: " + err);
} else {
if (config.debug)
console.log("save to db success");
logger.info("save to db success");
callback(null, result);
}
});
@ -125,20 +126,20 @@ function countFromDB(callback) {
if (err) {
client.end();
callback(err, null);
return console.error('could not connect to postgres', err);
return logger.error('could not connect to postgres', err);
}
client.query(countquery, function (err, result) {
client.end();
if (err) {
callback(err, null);
return console.error("count from db failed: " + err);
return logger.error("count from db failed: " + err);
} else {
//console.log(result.rows);
//logger.info(result.rows);
if (result.rows.length <= 0) {
callback("not found note in db", null);
} else {
if(config.debug)
console.log("count from db success");
logger.info("count from db success");
callback(null, result);
}
}

View file

@ -7,7 +7,8 @@ var logger = new winston.Logger({
level: 'debug',
handleExceptions: true,
json: false,
colorize: true
colorize: true,
timestamp: true
})
],
exitOnError: false

View file

@ -1,22 +1,56 @@
//note
//external modules
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var LZString = require('lz-string');
var marked = require('marked');
var cheerio = require('cheerio');
var shortId = require('shortid');
//others
var db = require("./db.js");
var logger = require("./logger.js");
//permission types
permissionTypes = ["freely", "editable", "locked"];
// create a note model
var model = mongoose.model('note', {
id: String,
shortid: {
type: String,
unique: true,
default: shortId.generate
},
permission: {
type: String,
enum: permissionTypes
},
viewcount: {
type: Number,
default: 0
},
updated: Date,
created: Date
});
//public
var note = {
model: model,
findNote: findNote,
newNote: newNote,
findOrNewNote: findOrNewNote,
checkNoteIdValid: checkNoteIdValid,
checkNoteExist: checkNoteExist,
getNoteTitle: getNoteTitle
getNoteTitle: getNoteTitle,
generateWebTitle: generateWebTitle,
increaseViewCount: increaseViewCount,
updatePermission: updatePermission
};
function checkNoteIdValid(noteId) {
try {
//console.log(noteId);
//logger.info(noteId);
var id = LZString.decompressFromBase64(noteId);
if (!id) return false;
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;
@ -26,21 +60,21 @@ function checkNoteIdValid(noteId) {
else
return false;
} catch (err) {
console.error(err);
logger.error(err);
return false;
}
}
function checkNoteExist(noteId) {
try {
//console.log(noteId);
//logger.info(noteId);
var id = LZString.decompressFromBase64(noteId);
db.readFromDB(id, function (err, result) {
if (err) return false;
return true;
});
} catch (err) {
console.error(err);
logger.error(err);
return false;
}
}
@ -50,11 +84,118 @@ function getNoteTitle(body) {
var $ = cheerio.load(marked(body));
var h1s = $("h1");
var title = "";
if (h1s.length > 0)
if (h1s.length > 0 && h1s.first().text().split('\n').length == 1)
title = h1s.first().text();
else
title = "Untitled";
return title;
}
//generate note web page title
function generateWebTitle(title) {
title = !title || title == "Untitled" ? "HackMD - Collaborative notes" : title + " - HackMD";
return title;
}
function findNote(id, callback) {
model.findOne({
$or: [
{
id: id
},
{
shortid: id
}
]
}, function (err, note) {
if (err) {
logger.error('find note failed: ' + err);
callback(err, null);
}
if (!err && note) {
callback(null, note);
} else {
logger.error('find note failed: ' + err);
callback(err, null);
};
});
}
function newNote(id, permission, callback) {
var note = new model({
id: id,
permission: permission,
updated: Date.now(),
created: Date.now()
});
note.save(function (err) {
if (err) {
logger.error('new note failed: ' + err);
callback(err, null);
} else {
logger.info("new note success: " + note.id);
callback(null, note);
};
});
}
function findOrNewNote(id, permission, callback) {
findNote(id, function (err, note) {
if (err || !note) {
newNote(id, permission, function (err, note) {
if (err) {
logger.error('find or new note failed: ' + err);
callback(err, null);
} else {
callback(null, note);
}
});
} else {
if (!note.permission) {
note.permission = permission;
note.updated = Date.now();
note.save(function (err) {
if (err) {
logger.error('add note permission failed: ' + err);
callback(err, null);
} else {
logger.info("add note permission success: " + note.id);
callback(null, note);
};
});
} else {
callback(null, note);
}
}
});
}
function increaseViewCount(note, callback) {
note.viewcount++;
note.updated = Date.now();
note.save(function (err) {
if (err) {
logger.error('increase note viewcount failed: ' + err);
callback(err, null);
} else {
logger.info("increase note viewcount success: " + note.id);
callback(null, note);
};
});
}
function updatePermission(note, permission, callback) {
note.permission = permission;
note.updated = Date.now();
note.save(function (err) {
if (err) {
logger.error('update note permission failed: ' + err);
callback(err, null);
} else {
logger.info("update note permission success: " + note.id);
callback(null, note);
};
});
}
module.exports = note;

View file

@ -9,9 +9,12 @@ var shortId = require('shortid');
var randomcolor = require("randomcolor");
var Chance = require('chance'),
chance = new Chance();
var md5 = require("blueimp-md5").md5;
var moment = require('moment');
//core
var config = require("../config.js");
var logger = require("./logger.js");
//others
var db = require("./db.js");
@ -49,7 +52,7 @@ function secure(socket, next) {
next(new Error('AUTH failed: No cookie transmitted.'));
}
if (config.debug)
console.log("AUTH success cookie: " + handshakeData.sessionID);
logger.info("AUTH success cookie: " + handshakeData.sessionID);
next();
} catch (ex) {
@ -65,9 +68,10 @@ var updater = setInterval(function () {
var note = notes[key];
if (note.isDirty) {
if (config.debug)
console.log("updater found dirty note: " + key);
logger.info("updater found dirty note: " + key);
var body = LZString.decompressFromUTF16(note.body);
var title = Note.getNoteTitle(body);
title = LZString.compressToBase64(title);
body = LZString.compressToBase64(body);
db.saveToDB(key, title, body,
function (err, result) {});
@ -75,36 +79,45 @@ var updater = setInterval(function () {
}
callback();
}, function (err) {
if (err) return console.error('updater error', err);
if (err) return logger.error('updater error', err);
});
}, 5000);
function getStatus(callback) {
db.countFromDB(function (err, data) {
if (err) return console.log(err);
var regusers = 0;
var distinctregusers = 0;
if (err) return logger.info(err);
var distinctaddresses = [];
var regaddresses = [];
var distinctregaddresses = [];
Object.keys(users).forEach(function (key) {
var value = users[key];
if (value.login)
regusers++;
var user = users[key];
var found = false;
for (var i = 0; i < distinctaddresses.length; i++) {
if (value.address == distinctaddresses[i]) {
if (user.address == distinctaddresses[i]) {
found = true;
break;
}
}
if (!found) {
distinctaddresses.push(value.address);
if (value.login)
distinctregusers++;
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);
}
}
});
User.getUserCount(function (err, regcount) {
if (err) {
console.log('get status failed: ' + err);
logger.error('get status failed: ' + err);
return;
}
if (callback)
@ -114,8 +127,8 @@ function getStatus(callback) {
distinctOnlineUsers: distinctaddresses.length,
notesCount: data.rows[0].count,
registeredUsers: regcount,
onlineRegisteredUsers: regusers,
distinctOnlineRegisteredUsers: distinctregusers
onlineRegisteredUsers: regaddresses.length,
distinctOnlineRegisteredUsers: distinctregaddresses.length
});
});
});
@ -146,23 +159,40 @@ function emitOnlineUsers(socket) {
if (user)
users.push(buildUserOutData(user));
});
notes[notename].socks.forEach(function (sock) {
var out = {
users: users
};
out = LZString.compressToUTF16(JSON.stringify(out));
sock.emit('online users', out);
});
var out = {
users: users
};
out = LZString.compressToUTF16(JSON.stringify(out));
for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
var sock = notes[notename].socks[i];
if (sock && out)
sock.emit('online users', out);
};
}
function emitUserStatus(socket) {
var notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
notes[notename].socks.forEach(function (sock) {
var out = buildUserOutData(users[socket.id]);
for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
var sock = notes[notename].socks[i];
if (sock != socket) {
var out = buildUserOutData(users[socket.id]);
sock.emit('user status', out);
}
};
}
function emitRefresh(socket) {
var notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
var note = notes[notename];
socket.emit('refresh', {
owner: note.owner,
permission: note.permission,
body: note.body,
otk: note.otk,
hash: note.hash,
updatetime: note.updatetime
});
}
@ -175,9 +205,7 @@ function finishConnection(socket, notename) {
notes[notename].users[socket.id] = users[socket.id];
notes[notename].socks.push(socket);
emitOnlineUsers(socket);
socket.emit('refresh', {
body: notes[notename].body
});
emitRefresh(socket);
//clear finished socket in queue
for (var i = 0; i < connectionSocketQueue.length; i++) {
@ -190,11 +218,11 @@ function finishConnection(socket, notename) {
startConnection(connectionSocketQueue[0]);
if (config.debug) {
console.log('SERVER connected a client to [' + notename + ']:');
console.log(JSON.stringify(users[socket.id]));
//console.log(notes);
logger.info('SERVER connected a client to [' + notename + ']:');
logger.info(JSON.stringify(users[socket.id]));
//logger.info(notes);
getStatus(function (data) {
console.log(JSON.stringify(data));
logger.info(JSON.stringify(data));
});
}
}
@ -219,17 +247,34 @@ function startConnection(socket) {
connectionSocketQueue.splice(i, 1);
}
isConnectionBusy = false;
return console.error(err);
return logger.error(err);
}
var body = LZString.decompressFromBase64(data.rows[0].content);
body = LZString.compressToUTF16(body);
notes[notename] = {
socks: [],
body: body,
isDirty: false,
users: {}
};
finishConnection(socket, notename);
var owner = data.rows[0].owner;
var permission = "freely";
if (owner && owner != "null") {
permission = "editable";
}
Note.findOrNewNote(notename, permission, function (err, note) {
if (err) {
responseError(res, "404", "Not Found", "oops.");
return;
}
var body = LZString.decompressFromBase64(data.rows[0].content);
body = LZString.compressToUTF16(body);
var updatetime = data.rows[0].update_time;
notes[notename] = {
owner: owner,
permission: note.permission,
socks: [],
body: body,
isDirty: false,
users: {},
otk: shortId.generate(),
hash: md5(body),
updatetime: moment(updatetime).valueOf()
};
finishConnection(socket, notename);
});
});
} else {
finishConnection(socket, notename);
@ -241,8 +286,8 @@ function disconnect(socket) {
isDisconnectBusy = true;
if (config.debug) {
console.log("SERVER disconnected a client");
console.log(JSON.stringify(users[socket.id]));
logger.info("SERVER disconnected a client");
logger.info(JSON.stringify(users[socket.id]));
}
var notename = getNotenameFromSocket(socket);
if (!notename) return;
@ -251,24 +296,31 @@ function disconnect(socket) {
}
if (notes[notename]) {
delete notes[notename].users[socket.id];
var index = notes[notename].socks.indexOf(socket);
if (index > -1) {
notes[notename].socks.splice(index, 1);
}
do {
var index = notes[notename].socks.indexOf(socket);
if (index != -1) {
notes[notename].socks.splice(index, 1);
}
} while (index != -1);
if (Object.keys(notes[notename].users).length <= 0) {
var body = LZString.decompressFromUTF16(notes[notename].body);
var title = Note.getNoteTitle(body);
body = LZString.compressToBase64(body);
db.saveToDB(notename, title, body,
function (err, result) {
delete notes[notename];
if (config.debug) {
//console.log(notes);
getStatus(function (data) {
console.log(JSON.stringify(data));
});
}
});
if (notes[notename].isDirty) {
var body = LZString.decompressFromUTF16(notes[notename].body);
var title = Note.getNoteTitle(body);
title = LZString.compressToBase64(title);
body = LZString.compressToBase64(body);
db.saveToDB(notename, title, body,
function (err, result) {
delete notes[notename];
if (config.debug) {
//logger.info(notes);
getStatus(function (data) {
logger.info(JSON.stringify(data));
});
}
});
} else {
delete notes[notename];
}
}
}
emitOnlineUsers(socket);
@ -284,9 +336,9 @@ function disconnect(socket) {
disconnect(disconnectSocketQueue[0]);
if (config.debug) {
//console.log(notes);
//logger.info(notes);
getStatus(function (data) {
console.log(JSON.stringify(data));
logger.info(JSON.stringify(data));
});
}
}
@ -309,6 +361,24 @@ function updateUserData(socket, user) {
//retrieve user data from passport
if (socket.request.user && socket.request.user.logged_in) {
var profile = JSON.parse(socket.request.user.profile);
/*
var photo = null;
switch(profile.provider) {
case "facebook":
console.log(profile);
break;
case "twitter":
photo = profile.photos[0];
break;
case "github":
photo = profile.avatar_url;
break;
case "dropbox":
//not image api provided
break;
}
user.photo = photo;
*/
user.name = profile.displayName || profile.username;
user.userid = socket.request.user._id;
user.login = true;
@ -353,7 +423,6 @@ function connection(socket) {
id: socket.id,
address: socket.handshake.address,
'user-agent': socket.handshake.headers['user-agent'],
otk: shortId.generate(),
color: color,
cursor: null,
login: false,
@ -368,24 +437,41 @@ function connection(socket) {
connectionSocketQueue.push(socket);
startConnection(socket);
//when a new client coming or received a client refresh request
socket.on('refresh', function (body_) {
//received client refresh request
socket.on('refresh', function () {
emitRefresh(socket);
});
//received client data updated
socket.on('update', function (body_) {
var notename = getNotenameFromSocket(socket);
if (!notename) return;
if (!notename || !notes[notename]) return;
if (config.debug)
console.log('SERVER received [' + notename + '] data updated: ' + socket.id);
if (notes[notename].body != body_) {
notes[notename].body = body_;
notes[notename].isDirty = true;
logger.info('SERVER received [' + notename + '] data updated: ' + socket.id);
var note = notes[notename];
if (note.body != body_) {
note.body = body_;
note.hash = md5(body_);
note.updatetime = Date.now();
note.isDirty = true;
}
var out = {
id: socket.id,
hash: note.hash,
updatetime: note.updatetime
};
for (var i = 0, l = note.socks.length; i < l; i++) {
var sock = note.socks[i];
sock.emit('check', out);
};
});
//received user status
socket.on('user status', function (data) {
var notename = getNotenameFromSocket(socket);
if (!notename) return;
if (!notename || !notes[notename]) return;
if (config.debug)
console.log('SERVER received [' + notename + '] user status from [' + socket.id + ']: ' + JSON.stringify(data));
logger.info('SERVER received [' + notename + '] user status from [' + socket.id + ']: ' + JSON.stringify(data));
if (data) {
var user = users[socket.id];
user.idle = data.idle;
@ -394,9 +480,40 @@ function connection(socket) {
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 notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
var note = notes[notename];
//Only owner can change permission
if (note.owner == socket.request.user._id) {
note.permission = permission;
Note.findNote(notename, function (err, _note) {
if (err || !_note) {
return;
}
Note.updatePermission(_note, permission, function (err, _note) {
if (err || !_note) {
return;
}
var out = {
permission: permission
};
for (var i = 0, l = note.socks.length; i < l; i++) {
var sock = note.socks[i];
sock.emit('permission', out);
};
});
});
}
}
});
//reveiced when user logout or changed
socket.on('user changed', function () {
console.log('user changed');
logger.info('user changed');
var notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
updateUserData(socket, notes[notename].users[socket.id]);
@ -431,11 +548,12 @@ function connection(socket) {
if (!notename || !notes[notename]) return;
users[socket.id].cursor = data;
var out = buildUserOutData(users[socket.id]);
notes[notename].socks.forEach(function (sock) {
for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
var sock = notes[notename].socks[i];
if (sock != socket) {
sock.emit('cursor focus', out);
}
});
};
});
//received cursor activity
@ -444,11 +562,12 @@ function connection(socket) {
if (!notename || !notes[notename]) return;
users[socket.id].cursor = data;
var out = buildUserOutData(users[socket.id]);
notes[notename].socks.forEach(function (sock) {
for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
var sock = notes[notename].socks[i];
if (sock != socket) {
sock.emit('cursor activity', out);
}
});
};
});
//received cursor blur
@ -459,13 +578,12 @@ function connection(socket) {
var out = {
id: socket.id
};
notes[notename].socks.forEach(function (sock) {
for (var i = 0, l = notes[notename].socks.length; i < l; i++) {
var sock = notes[notename].socks[i];
if (sock != socket) {
if (sock != socket) {
sock.emit('cursor blur', out);
}
sock.emit('cursor blur', out);
}
});
};
});
//when a new client disconnect
@ -477,12 +595,30 @@ function connection(socket) {
//when received client change data request
socket.on('change', function (op) {
var notename = getNotenameFromSocket(socket);
if (!notename) return;
if (!notename || !notes[notename]) return;
var note = notes[notename];
switch (note.permission) {
case "freely":
//not blocking anyone
break;
case "editable":
//only login user can change
if (!socket.request.user || !socket.request.user.logged_in)
return;
break;
case "locked":
//only owner can change
if (note.owner != socket.request.user._id)
return;
break;
}
op = LZString.decompressFromUTF16(op);
if (op)
op = JSON.parse(op);
else
return;
if (config.debug)
console.log('SERVER received [' + notename + '] data changed: ' + socket.id + ', op:' + JSON.stringify(op));
logger.info('SERVER received [' + notename + '] data changed: ' + socket.id + ', op:' + JSON.stringify(op));
switch (op.origin) {
case '+input':
case '+delete':
@ -499,16 +635,20 @@ function connection(socket) {
case '+joinLines':
case '+duplicateLine':
case '+sortLines':
notes[notename].socks.forEach(function (sock) {
if (sock != socket) {
if (config.debug)
console.log('SERVER emit sync data out [' + notename + ']: ' + sock.id + ', op:' + JSON.stringify(op));
sock.emit('change', LZString.compressToUTF16(JSON.stringify(op)));
}
});
op.id = socket.id;
op.otk = note.otk;
op.nextotk = note.otk = shortId.generate();
var stringop = JSON.stringify(op);
var compressstringop = LZString.compressToUTF16(stringop);
for (var i = 0, l = note.socks.length; i < l; i++) {
var sock = note.socks[i];
if (config.debug)
logger.info('SERVER emit sync data out [' + notename + ']: ' + sock.id + ', op:' + stringop);
sock.emit('change', compressstringop);
};
break;
default:
console.log('SERVER received uncaught [' + notename + '] data changed: ' + socket.id + ', op:' + JSON.stringify(op));
default:
logger.info('SERVER received uncaught [' + notename + '] data changed: ' + socket.id + ', op:' + JSON.stringify(op));
}
});
}

View file

@ -6,6 +6,8 @@ var path = require('path');
var uuid = require('node-uuid');
var markdownpdf = require("markdown-pdf");
var LZString = require('lz-string');
var S = require('string');
var shortId = require('shortid');
//core
var config = require("../config.js");
@ -31,16 +33,20 @@ var response = {
newNote: newNote,
showFeatures: showFeatures,
showNote: showNote,
noteActions: noteActions
showShareNote: showShareNote,
noteActions: noteActions,
shareNoteActions: shareNoteActions
};
function responseError(res, code, detail, msg) {
res.writeHead(code, {
'Content-Type': 'text/html'
});
var content = ejs.render(fs.readFileSync(config.errorpath, 'utf8'), {
var template = config.errorpath;
var content = ejs.render(fs.readFileSync(template, 'utf8'), {
title: code + ' ' + detail + ' ' + msg,
cache: !config.debug,
filename: config.errorpath,
filename: template,
code: code,
detail: detail,
msg: msg
@ -49,16 +55,44 @@ function responseError(res, code, detail, msg) {
res.end();
}
function responseHackMD(res) {
res.writeHead(200, {
'Content-Type': 'text/html'
function responseHackMD(res, noteId) {
if (noteId != config.featuresnotename) {
if (!Note.checkNoteIdValid(noteId)) {
responseError(res, "404", "Not Found", "oops.");
return;
}
noteId = LZString.decompressFromBase64(noteId);
if (!noteId) {
responseError(res, "404", "Not Found", "oops.");
return;
}
}
db.readFromDB(noteId, function (err, data) {
if (err) {
responseError(res, "404", "Not Found", "oops.");
return;
}
var title = data.rows[0].title;
var decodedTitle = LZString.decompressFromBase64(title);
if (decodedTitle) title = decodedTitle;
title = Note.generateWebTitle(title);
var template = config.hackmdpath;
var options = {
cache: !config.debug,
filename: template
};
var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
var html = compiled({
title: title
});
var buf = html;
res.writeHead(200, {
'Content-Type': 'text/html; charset=UTF-8',
'Cache-Control': 'private',
'Content-Length': buf.length
});
res.end(buf);
});
var content = ejs.render(fs.readFileSync(config.hackmdpath, 'utf8'), {
cache: !config.debug,
filename: config.hackmdpath
});
res.write(content);
res.end();
}
function newNote(req, res, next) {
@ -88,10 +122,10 @@ function showFeatures(req, res, next) {
responseError(res, "500", "Internal Error", "wtf.");
return;
}
responseHackMD(res);
responseHackMD(res, config.featuresnotename);
});
} else {
responseHackMD(res);
responseHackMD(res, config.featuresnotename);
}
});
}
@ -102,22 +136,105 @@ function showNote(req, res, next) {
responseError(res, "404", "Not Found", "oops.");
return;
}
responseHackMD(res);
responseHackMD(res, noteId);
}
function showShareNote(req, res, next) {
var shortid = req.params.shortid;
if (shortId.isValid(shortid)) {
Note.findNote(shortid, function (err, note) {
if (err || !note) {
responseError(res, "404", "Not Found", "oops.");
return;
}
//increase note viewcount
Note.increaseViewCount(note, function (err, note) {
if (err || !note) {
responseError(res, "404", "Not Found", "oops.");
return;
}
db.readFromDB(note.id, function (err, data) {
if (err) {
responseError(res, "404", "Not Found", "oops.");
return;
}
var body = LZString.decompressFromBase64(data.rows[0].content);
var updatetime = data.rows[0].update_time;
var text = S(body).escapeHTML().s;
var title = data.rows[0].title;
var decodedTitle = LZString.decompressFromBase64(title);
if (decodedTitle) title = decodedTitle;
title = Note.generateWebTitle(title);
var template = config.prettypath;
var options = {
cache: !config.debug,
filename: template
};
var compiled = ejs.compile(fs.readFileSync(template, 'utf8'), options);
var origin = "//" + req.headers.host;
var html = compiled({
title: title,
viewcount: note.viewcount,
updatetime: updatetime,
url: origin,
body: text
});
var buf = html;
res.writeHead(200, {
'Content-Type': 'text/html; charset=UTF-8',
'Cache-Control': 'private',
'Content-Length': buf.length
});
res.end(buf);
});
});
});
} else {
responseError(res, "404", "Not Found", "oops.");
}
}
function actionShare(req, res, noteId) {
db.readFromDB(noteId, function (err, data) {
if (err) {
responseError(res, "404", "Not Found", "oops.");
return;
}
var owner = data.rows[0].owner;
var permission = "freely";
if (owner && owner != "null") {
permission = "editable";
}
Note.findOrNewNote(noteId, permission, function (err, note) {
if (err) {
responseError(res, "404", "Not Found", "oops.");
return;
}
res.redirect("/s/" + note.shortid);
});
});
}
//pretty api is deprecated
function actionPretty(req, res, noteId) {
db.readFromDB(noteId, function (err, data) {
if (err) {
responseError(res, "404", "Not Found", "oops.");
return;
}
var body = data.rows[0].content;
var body = LZString.decompressFromBase64(data.rows[0].content);
var text = S(body).escapeHTML().s;
var title = data.rows[0].title;
var decodedTitle = LZString.decompressFromBase64(title);
if (decodedTitle) title = decodedTitle;
title = Note.generateWebTitle(title);
var template = config.prettypath;
var compiled = ejs.compile(fs.readFileSync(template, 'utf8'));
var origin = "//" + req.headers.host;
var html = compiled({
title: title,
url: origin,
body: body
body: text
});
var buf = html;
res.writeHead(200, {
@ -190,8 +307,9 @@ function noteActions(req, res, next) {
}
var action = req.params.action;
switch (action) {
case "pretty":
actionPretty(req, res, noteId);
case "share":
case "pretty": //pretty deprecated
actionShare(req, res, noteId);
break;
case "download":
actionDownload(req, res, noteId);
@ -208,4 +326,25 @@ function noteActions(req, res, next) {
}
}
function shareNoteActions(req, res, next) {
var action = req.params.action;
switch (action) {
case "edit":
var shortid = req.params.shortid;
if (shortId.isValid(shortid)) {
Note.findNote(shortid, function (err, note) {
if (err || !note) {
responseError(res, "404", "Not Found", "oops.");
return;
}
if (note.id != config.featuresnotename)
res.redirect('/' + LZString.compressToBase64(note.id));
else
res.redirect('/' + note.id);
});
}
break;
}
}
module.exports = response;

View file

@ -4,6 +4,7 @@ var mongoose = require('mongoose');
//core
var config = require("../config.js");
var logger = require("./logger.js");
// create a temp model
var model = mongoose.model('temp', {
@ -33,13 +34,13 @@ function findTemp(id, callback) {
id: id
}, function (err, temp) {
if (err) {
console.log('find temp failed: ' + err);
logger.error('find temp failed: ' + err);
callback(err, null);
}
if (!err && temp) {
callback(null, temp);
} else {
console.log('find temp failed: ' + err);
logger.error('find temp failed: ' + err);
callback(err, null);
};
});
@ -53,10 +54,10 @@ function newTemp(id, data, callback) {
});
temp.save(function (err) {
if (err) {
console.log('new temp failed: ' + err);
logger.error('new temp failed: ' + err);
callback(err, null);
} else {
console.log("new temp success: " + temp.id);
logger.info("new temp success: " + temp.id);
callback(null, temp);
};
});
@ -67,14 +68,14 @@ function removeTemp(id, callback) {
if(!err && temp) {
temp.remove(function(err) {
if(err) {
console.log('remove temp failed: ' + err);
logger.error('remove temp failed: ' + err);
callback(err, null);
} else {
callback(null, null);
}
});
} else {
console.log('remove temp failed: ' + err);
logger.error('remove temp failed: ' + err);
callback(err, null);
}
});

View file

@ -4,6 +4,7 @@ var mongoose = require('mongoose');
//core
var config = require("../config.js");
var logger = require("./logger.js");
// create a user model
var model = mongoose.model('user', {
@ -34,13 +35,13 @@ function findUser(id, callback) {
id: id
}, function (err, user) {
if (err) {
console.log('find user failed: ' + err);
logger.error('find user failed: ' + err);
callback(err, null);
}
if (!err && user) {
callback(null, user);
} else {
console.log('find user failed: ' + err);
logger.error('find user failed: ' + err);
callback(err, null);
};
});
@ -54,10 +55,10 @@ function newUser(id, profile, callback) {
});
user.save(function (err) {
if (err) {
console.log('new user failed: ' + err);
logger.error('new user failed: ' + err);
callback(err, null);
} else {
console.log("new user success: " + user.id);
logger.info("new user success: " + user.id);
callback(null, user);
};
});
@ -68,7 +69,7 @@ function findOrNewUser(id, profile, callback) {
if(err || !user) {
newUser(id, profile, function(err, user) {
if(err) {
console.log('find or new user failed: ' + err);
logger.error('find or new user failed: ' + err);
callback(err, null);
} else {
callback(null, user);

0
logs/.keep Normal file
View file

View file

@ -1,6 +1,6 @@
{
"name": "hackmd",
"version": "0.2.9",
"version": "0.3.1",
"description": "Realtime collaborative markdown notes on all platforms.",
"main": "app.js",
"author": "jackycute",
@ -8,6 +8,7 @@
"license": "MIT",
"dependencies": {
"async": "^0.9.0",
"blueimp-md5": "^1.1.0",
"body-parser": "^1.12.3",
"chance": "^0.7.5",
"cheerio": "^0.19.0",
@ -27,6 +28,7 @@
"markdown-pdf": "^5.2.0",
"marked": "^0.3.3",
"method-override": "^2.3.2",
"moment": "^2.10.3",
"mongoose": "^4.0.2",
"morgan": "^1.5.3",
"node-uuid": "^1.4.3",
@ -38,8 +40,9 @@
"passport.socketio": "^3.5.1",
"pg": "4.x",
"randomcolor": "^0.2.0",
"shortid": "2.1.3",
"shortid": "2.2.2",
"socket.io": "1.3.5",
"string": "^3.2.0",
"toobusy-js": "^0.4.1",
"winston": "^1.0.0"
},

19
processes.json Normal file
View file

@ -0,0 +1,19 @@
{
"apps": [{
"name": "hackmd",
"script": "app.js",
"exec_mode": "fork",
"instances": 1,
"error_file": "./logs/hackmd-err.log",
"out_file": "./logs/hackmd-out.log",
"pid_file": "./hackmd.pid",
"env": {
"NODE_ENV": "production",
"DATABASE_URL": "change this",
"MONGOLAB_URI": "change this",
"PORT": "80",
"SSLPORT": "443",
"DOMAIN": "change this"
}
}]
}

View file

@ -223,10 +223,21 @@ input {
border-radius: 5px;
color: black;
text-shadow: none;
min-height: 134px;
display: table;
min-width: 100%;
}
.list li .item .tags {
.list li .item .content {
display: table-cell;
vertical-align: middle;
}
.list li .item .content .tags {
line-height: 25px;
}
.list li .item .content .tags span {
display: inline-block;
line-height: 15px;
}
.form-inline {
padding: 0 10px;
}
@ -246,6 +257,10 @@ input {
.ui-history-close:hover {
opacity: 1;
}
.ui-or {
margin-top: 5px;
margin-bottom: 5px;
}
.modal-title {
text-align: left;

View file

@ -13,15 +13,21 @@
}
.vimeo .icon,
.youtube .icon {
opacity: 0.5;
opacity: 0.3;
display: table-cell;
vertical-align: middle;
height: inherit;
margin: 0 auto;
color: white;
-webkit-transition: opacity 0.2s; /* Safari */
transition: opacity 0.2s;
}
.vimeo:hover .icon,
.youtube:hover .icon {
opacity: 0.6;
-webkit-transition: opacity 0.2s; /* Safari */
transition: opacity 0.2s;
}
h1:hover .header-link,
h2:hover .header-link,
@ -44,4 +50,168 @@ h6:hover .header-link {
-moz-transition: opacity 0.2s ease-in-out 0.1s;
-o-transition: opacity 0.2s ease-in-out 0.1s;
transition: opacity 0.2s ease-in-out 0.1s;
}
.ui-infobar {
max-width: 758px;
margin-top: 25px;
margin-bottom: -25px;
color: #777;
}
.ui-toc {
position: fixed;
bottom: 20px;
z-index: 10000;
}
.ui-toc-label {
opacity: 0.3;
background-color: #ccc;
border: none;
-webkit-transition: opacity 0.2s; /* Safari */
transition: opacity 0.2s;
}
.ui-toc .open .ui-toc-label {
opacity: 1;
color: white;
-webkit-transition: opacity 0.2s; /* Safari */
transition: opacity 0.2s;
}
.ui-toc-label:focus {
opacity: 0.3;
background-color: #ccc;
color: black;
}
.ui-toc-label:hover {
opacity: 1;
background-color: #ccc;
-webkit-transition: opacity 0.2s; /* Safari */
transition: opacity 0.2s;
}
.ui-toc-dropdown {
margin-top: 20px;
margin-bottom: 20px;
padding-left: 10px;
padding-right: 10px;
max-width: 45vw;
width: 25vw;
max-height: 65vh;
overflow: auto;
}
.ui-toc-dropdown a {
overflow: hidden;
text-overflow: ellipsis;
white-space: pre;
}
.ui-toc-dropdown .nav>li>a {
display: block;
padding: 4px 20px;
font-size: 13px;
font-weight: 500;
color: #767676;
}
.ui-toc-dropdown .nav>li>a:focus,.ui-toc-dropdown .nav>li>a:hover {
padding-left: 19px;
color: black;
text-decoration: none;
background-color: transparent;
border-left: 1px solid black;
}
.ui-toc-dropdown .nav>.active:focus>a,.ui-toc-dropdown .nav>.active:hover>a,.ui-toc-dropdown .nav>.active>a {
padding-left: 18px;
font-weight: 700;
color: black;
background-color: transparent;
border-left: 2px solid black;
}
.ui-toc-dropdown .nav .nav {
display: none;
padding-bottom: 10px;
}
.ui-toc-dropdown .nav>.active>ul {
display: block;
}
.ui-toc-dropdown .nav .nav>li>a {
padding-top: 1px;
padding-bottom: 1px;
padding-left: 30px;
font-size: 12px;
font-weight: 400;
}
.ui-toc-dropdown .nav .nav>li>ul>li>a {
padding-top: 1px;
padding-bottom: 1px;
padding-left: 40px;
font-size: 12px;
font-weight: 400;
}
.ui-toc-dropdown .nav .nav>li>a:focus,.ui-toc-dropdown .nav .nav>li>a:hover {
padding-left: 29px;
}
.ui-toc-dropdown .nav .nav>li>ul>li>a:focus,.ui-toc-dropdown .nav .nav>li>ul>li>a:hover {
padding-left: 39px;
}
.ui-toc-dropdown .nav .nav>.active:focus>a,.ui-toc-dropdown .nav .nav>.active:hover>a,.ui-toc-dropdown .nav .nav>.active>a {
padding-left: 28px;
font-weight: 500;
}
.ui-toc-dropdown .nav .nav>.active>.nav>.active:focus>a,.ui-toc-dropdown .nav .nav>.active>.nav>.active:hover>a,.ui-toc-dropdown .nav .nav>.active>.nav>.active>a {
padding-left: 38px;
font-weight: 500;
}
.ui-affix-toc {
position: fixed;
top: 0;
max-width: 15vw;
max-height: 70vh;
overflow: auto;
}
.back-to-top, .go-to-bottom {
display: block;
padding: 4px 10px;
margin-top: 10px;
margin-left: 10px;
font-size: 12px;
font-weight: 500;
color: #999;
}
.back-to-top:hover, .back-to-top:focus, .go-to-bottom:hover, .go-to-bottom:focus {
color: #563d7c;
text-decoration: none;
}
.go-to-bottom {
margin-top: 0;
}
small span {
line-height: 22px;
}
small .dropdown {
display: inline-block;
}
small .dropdown a:focus, small .dropdown a:hover {
text-decoration: none;
}

View file

@ -177,11 +177,11 @@ div[contenteditable]:empty:not(:focus):before{
content:attr(data-ph);
color: gray;
}
.dropdown-menu {
.dropdown-menu.list {
max-height: 80vh;
overflow: auto;
}
.dropdown-menu::-webkit-scrollbar {
.dropdown-menu.list::-webkit-scrollbar {
display: none;
}
.dropdown-menu .emoji {
@ -201,6 +201,26 @@ div[contenteditable]:empty:not(:focus):before{
user-select: none;
}
.btn-file {
position: relative;
overflow: hidden;
}
.btn-file input[type=file] {
position: absolute;
top: 0;
right: 0;
min-width: 100%;
min-height: 100%;
font-size: 100px;
text-align: right;
filter: alpha(opacity=0);
opacity: 0;
outline: none;
background: white;
cursor: inherit;
display: block;
}
.cm-trailing-space-a:before,
.cm-trailing-space-b:before,
.cm-trailing-space-new-line:before {

View file

@ -13,4 +13,10 @@ body {
}
::-moz-focus-inner {
border: 0 !important;
}
/* manual fix for bootstrap issue 14040, there is an unnecessary padding-right on modal open */
body.modal-open {
overflow-y: auto;
padding-right: 0 !important;
}

View file

@ -52,7 +52,7 @@
<p class="lead">
Realtime collaborative markdown notes on all platforms.
</p>
<a type="button" class="btn btn-lg btn-success ui-signin" data-toggle="modal" data-target=".bs-example-modal-sm" style="display:none;">Sign In</a>
<a type="button" class="btn btn-lg btn-success ui-signin" data-toggle="modal" data-target=".signin-modal" style="display:none;">Sign In</a>
<div class="ui-or" style="display:none;">Or</div>
<p class="lead">
<a href="/new" class="btn btn-lg btn-default">Start new note</a>
@ -66,7 +66,7 @@
<div id="history" class="section" style="display:none;">
<div class="ui-signin">
<h4>
<a type="button" class="btn btn-success" data-toggle="modal" data-target=".bs-example-modal-sm">Sign In</a> to get own history!
<a type="button" class="btn btn-success" data-toggle="modal" data-target=".signin-modal">Sign In</a> to get own history!
</h4>
<p>Below are history from browser</p>
</div>
@ -98,7 +98,7 @@
<span class="btn btn-default btn-file ui-open-history" title="Import history">
<i class="fa fa-folder-open-o"></i><input type="file" />
</span>
<a href="#" class="btn btn-default ui-clear-history" title="Clear history"><i class="fa fa-trash-o"></i></a>
<a href="#" class="btn btn-default ui-clear-history" title="Clear history" data-toggle="modal" data-target=".delete-modal"><i class="fa fa-trash-o"></i></a>
</span>
<a href="#" class="btn btn-default ui-refresh-history" title="Refresh history"><i class="fa fa-refresh"></i></a>
</form>
@ -143,9 +143,7 @@
<div class="mastfoot">
<div class="inner">
<h6>
<div class="fb-like" data-href="https://www.facebook.com/TakeHackMD" data-width="80" data-layout="button_count" data-action="like" data-show-faces="true" data-share="false" style="vertical-align:middle;"></div>
&nbsp;
<iframe src="//ghbtns.com/github-btn.html?user=jackycute&repo=hackmd&type=star&count=true" frameborder="0" scrolling="0" width="80px" height="20px" style="vertical-align:middle;"></iframe>
<iframe src="//ghbtns.com/github-btn.html?user=jackycute&repo=hackmd&type=star&count=true" frameborder="0" scrolling="0" width="85px" height="20px" style="vertical-align:middle;"></iframe>
</h6>
<p>&copy; 2015 <a href="https://www.facebook.com/TakeHackMD" target="_blank"><i class="fa fa-facebook-square"></i> HackMD</a> by <a href="https://github.com/jackycute" target="_blank"><i class="fa fa-github-square"></i> jackycute</a>
</p>
@ -156,7 +154,7 @@
</div>
<!-- signin modal -->
<div class="modal fade bs-example-modal-sm" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
<div class="modal fade signin-modal" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
@ -181,12 +179,30 @@
</div>
</div>
</div>
<div id="fb-root"></div>
<!-- delete modal -->
<div class="modal fade delete-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="myModalLabel">Are you sure?</h4>
</div>
<div class="modal-body" style="color:black;">
<h5 class="ui-delete-modal-msg"></h5>
<strong class="ui-delete-modal-item"></strong>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-danger ui-delete-modal-confirm">Yes, do it!</button>
</div>
</div>
</div>
</div>
<!-- Bootstrap core JavaScript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="/js/fb.js" async defer></script>
<script src="//code.jquery.com/jquery-1.11.3.min.js" defer></script>
<script src="/vendor/greensock-js/TweenMax.min.js" defer></script>
<script src="/vendor/greensock-js/jquery.gsap.min.js" defer></script>

View file

@ -4,12 +4,17 @@ var options = {
<span class="id" style="display:none;"></span>\
<a href="#">\
<div class="item">\
<div class="ui-history-close fa fa-close fa-fw"></div>\
<h4 class="text"></h4>\
<p><i class="fromNow"><i class="fa fa-clock-o"></i></i>\
<br>\
<i class="timestamp" style="display:none;"></i><i class="time"></i></p>\
<p class="tags"></p>\
<div class="ui-history-close fa fa-close fa-fw" data-toggle="modal" data-target=".delete-modal"></div>\
<div class="content">\
<h4 class="text"></h4>\
<p>\
<i><i class="fa fa-clock-o"></i> visit </i><i class="fromNow"></i>\
<br>\
<i class="timestamp" style="display:none;"></i>\
<i class="time"></i>\
</p>\
<p class="tags"></p>\
</div>\
</div>\
</a>\
</li>'
@ -114,16 +119,53 @@ function parseHistoryCallback(list, notehistory) {
$(".ui-history-close").click(function (e) {
e.preventDefault();
var id = $(this).closest("a").siblings("span").html();
getHistory(function (notehistory) {
var newnotehistory = removeHistory(id, notehistory);
saveHistory(newnotehistory);
});
list.remove('id', id);
checkHistoryList();
var value = list.get('id', id)[0].values();
$('.ui-delete-modal-msg').text('Do you really want to delete below history?');
$('.ui-delete-modal-item').html('<i class="fa fa-file-text"></i> ' + value.text + '<br><i class="fa fa-clock-o"></i> ' + value.time);
clearHistory = false;
deleteId = id;
});
buildTagsFilter(filtertags);
}
//auto update item fromNow every minutes
setInterval(updateItemFromNow, 60000);
function updateItemFromNow() {
var items = $('.item').toArray();
for (var i = 0; i < items.length; i++) {
var item = $(items[i]);
var timestamp = parseInt(item.find('.timestamp').text());
item.find('.fromNow').text(moment(timestamp).fromNow());
}
}
var clearHistory = false;
var deleteId = null;
function deleteHistory() {
if (clearHistory) {
saveHistory([]);
historyList.clear();
checkHistoryList();
} else {
if (!deleteId) return;
getHistory(function (notehistory) {
var newnotehistory = removeHistory(deleteId, notehistory);
saveHistory(newnotehistory);
});
historyList.remove('id', deleteId);
checkHistoryList();
}
$('.delete-modal').modal('hide');
clearHistory = false;
deleteId = null;
}
$(".ui-delete-modal-confirm").click(function () {
deleteHistory();
});
$(".ui-import-from-browser").click(function () {
saveStorageHistoryToServer(function () {
parseStorageToHistory(historyList, parseHistoryCallback);
@ -160,9 +202,10 @@ $(".ui-open-history").bind("change", function (e) {
});
$(".ui-clear-history").click(function () {
saveHistory([]);
historyList.clear();
checkHistoryList();
$('.ui-delete-modal-msg').text('Do you really want to clear all history?');
$('.ui-delete-modal-item').html('There is no turning back.');
clearHistory = true;
deleteId = null;
});
$(".ui-refresh-history").click(function () {
@ -229,6 +272,67 @@ var source = $("#template").html();
var template = Handlebars.compile(source);
var context = {
release: [
{
version: "0.3.1",
tag: "clearsky",
date: moment("201506301600", 'YYYYMMDDhhmm').fromNow(),
detail: [
{
title: "Features",
item: [
"+ Added auto table of content",
"+ Added basic permission control",
"+ Added view count in share note"
]
},
{
title: "Enhancements",
item: [
"* Toolbar now will hide in single view",
"* History time now will auto update",
"* Smooth scroll on anchor changed",
"* Updated video style"
]
},
{
title: "Fixes",
item: [
"* Note might not clear when all users disconnect",
"* Blockquote tag not parsed properly",
"* History style not correct"
]
}
]
},
{
version: "0.3.0",
tag: "sunrise",
date: moment("201506152400", 'YYYYMMDDhhmm').fromNow(),
detail: [
{
title: "Enhancements",
item: [
"* Used short url in share notes",
"* Added upload image button on toolbar",
"* Share notes are now SEO and mobile friendly",
"* Updated code block style",
"* Newline now will cause line breaks",
"* Image now will link out",
"* Used otk to avoid race condition",
"* Used hash to avoid data inconsistency",
"* Optimized server realtime script"
]
},
{
title: "Fixes",
item: [
"* Composition input might lost or duplicated when other input involved",
"* Note title might not save properly",
"* Todo list not render properly"
]
}
]
},
{
version: "0.2.9",
tag: "wildfire",

View file

@ -1,43 +1,75 @@
//auto update last change
var lastchangetime = null;
var lastchangeui = null;
function updateLastChange() {
if (lastchangetime && lastchangeui) {
lastchangeui.html('&nbsp;<i class="fa fa-clock-o"></i> change ' + moment(lastchangetime).fromNow());
lastchangeui.attr('title', moment(lastchangetime).format('llll'));
}
}
setInterval(updateLastChange, 60000);
//get title
function getTitle(view) {
var h1s = view.find("h1");
var title = "";
if (h1s.length > 0) {
if (h1s.length > 0) {
title = h1s.first().text();
} else {
title = null;
}
return title;
}
//render title
function renderTitle(view) {
var title = getTitle(view);
if (title) {
if (title) {
title += ' - HackMD';
} else {
title = 'HackMD - Collaborative notes';
}
return title;
}
//render filename
function renderFilename(view) {
var filename = getTitle(view);
if (!filename) {
if (!filename) {
filename = 'Untitled';
}
return filename;
}
function slugifyWithUTF8(text) {
var newText = S(text.toLowerCase()).trim().stripTags().dasherize().s;
newText = newText.replace(/([\!\"\#\$\%\&\'\(\)\*\+\,\.\/\:\;\<\=\>\?\@\[\\\]\^\`\{\|\}\~])/g, '');
return newText;
}
var viewAjaxCallback = null;
//regex for blockquote
var spaceregex = /\s*/;
var notinhtmltagregex = /(?![^<]*>|[^<>]*<\/)/;
var coloregex = /\[color=([#|\(|\)|\s|\,|\w]*?)\]/;
coloregex = new RegExp(coloregex.source + notinhtmltagregex.source, "g");
var nameregex = /\[name=(.*?)\]/;
var timeregex = /\[time=([:|,|+|-|\(|\)|\s|\w]*?)\]/;
var nameandtimeregex = new RegExp(nameregex.source + spaceregex.source + timeregex.source + notinhtmltagregex.source, "g");
nameregex = new RegExp(nameregex.source + notinhtmltagregex.source, "g");
timeregex = new RegExp(timeregex.source + notinhtmltagregex.source, "g");
//dynamic event or object binding here
function finishView(view) {
//youtube
view.find(".youtube").click(function () {
imgPlayiframe(this, '//www.youtube.com/embed/');
});
view.find(".youtube.raw").removeClass("raw")
.click(function () {
imgPlayiframe(this, '//www.youtube.com/embed/');
});
//vimeo
view.find(".vimeo")
view.find(".vimeo.raw").removeClass("raw")
.click(function () {
imgPlayiframe(this, '//player.vimeo.com/video/');
})
@ -54,35 +86,32 @@ function finishView(view) {
});
});
//gist
view.find("code[data-gist-id]").each(function(key, value) {
if($(value).children().length == 0)
view.find("code[data-gist-id]").each(function (key, value) {
if ($(value).children().length == 0)
$(value).gist(viewAjaxCallback);
});
//emojify
emojify.run(view[0]);
//mathjax
var mathjaxdivs = view.find('.mathjax').toArray();
var mathjaxdivs = view.find('.mathjax.raw').removeClass("raw").toArray();
try {
for (var i = 0; i < mathjaxdivs.length; i++) {
MathJax.Hub.Queue(["Typeset", MathJax.Hub, mathjaxdivs[i].innerHTML]);
MathJax.Hub.Queue(viewAjaxCallback);
$(mathjaxdivs[i]).removeClass("mathjax");
}
} catch(err) {
}
} catch (err) {}
//sequence diagram
var sequence = view.find(".sequence-diagram");
var sequence = view.find(".sequence-diagram.raw").removeClass("raw");
try {
sequence.sequenceDiagram({
theme: 'simple'
});
sequence.parent().parent().replaceWith(sequence);
sequence.removeClass("sequence-diagram");
} catch(err) {
} catch (err) {
console.error(err);
}
//flowchart
var flow = view.find(".flow-chart");
var flow = view.find(".flow-chart.raw").removeClass("raw");
flow.each(function (key, value) {
try {
var chart = flowchart.parse($(value).text());
@ -94,26 +123,41 @@ function finishView(view) {
'font-family': "'Andale Mono', monospace"
});
$(value).parent().parent().replaceWith(value);
$(value).removeClass("flow-chart");
} catch(err) {
} catch (err) {
console.error(err);
}
});
//image href new window(emoji not included)
var images = view.find("p > img[src]:not([class])");
images.each(function (key, value) {
var src = $(value).attr('src');
var a = $('<a>');
if (src) {
a.attr('href', src);
a.attr('target', "_blank");
}
a.html($(value).clone());
$(value).replaceWith(a);
});
//blockquote
var blockquote = view.find("blockquote.raw").removeClass("raw");
var blockquote_p = blockquote.find("p");
blockquote_p.each(function (key, value) {
var html = $(value).html();
html = html.replace(coloregex, '<span class="color" data-color="$1"></span>');
html = html.replace(nameandtimeregex, '<small><i class="fa fa-user"></i> $1 <i class="fa fa-clock-o"></i> $2</small>');
html = html.replace(nameregex, '<small><i class="fa fa-user"></i> $1</small>');
html = html.replace(timeregex, '<small><i class="fa fa-clock-o"></i> $1</small>');
$(value).html(html);
});
var blockquote_color = blockquote.find(".color");
blockquote_color.each(function (key, value) {
$(value).closest("blockquote").css('border-left-color', $(value).attr('data-color'));
});
//render title
document.title = renderTitle(view);
}
//regex for blockquote
var spaceregex = /\s*/;
var notinhtmltagregex = /(?![^<]*>|[^<>]*<\/)/;
var coloregex = /\[color=([#|\(|\)|\s|\,|\w]*)\]/;
coloregex = new RegExp(coloregex.source + notinhtmltagregex.source, "g");
var nameregex = /\[name=([-|_|\s|\w]*)\]/;
var timeregex = /\[time=([:|,|+|-|\(|\)|\s|\w]*)\]/;
var 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");
//only static transform should be here
function postProcess(code) {
var result = $('<div>' + code + '</div>');
@ -125,32 +169,107 @@ function postProcess(code) {
return "<noiframe>" + $(this).html() + "</noiframe>"
});
//todo list
var lis = result[0].getElementsByTagName('li');
var lis = result.find('li.raw').removeClass("raw").sortByDepth().toArray();
for (var i = 0; i < lis.length; i++) {
var html = lis[i].innerHTML;
if (/^\s*\[[x ]\]\s+/.test(html)) {
lis[i].innerHTML = html.replace(/^\s*\[ \]\s*/, '<input type="checkbox" class="task-list-item-checkbox" disabled>')
.replace(/^\s*\[x\]\s*/, '<input type="checkbox" class="task-list-item-checkbox" checked disabled>');
var li = lis[i];
var html = $(li).clone()[0].innerHTML;
var p = $(li).children('p');
if (p.length == 1) {
html = p.html();
li = p[0];
}
if (/^\s*\[[x ]\]\s*/.test(html)) {
li.innerHTML = html.replace(/^\s*\[ \]\s*/, '<input type="checkbox" class="task-list-item-checkbox" disabled><label></label>')
.replace(/^\s*\[x\]\s*/, '<input type="checkbox" class="task-list-item-checkbox" checked disabled><label></label>');
lis[i].setAttribute('class', 'task-list-item');
}
}
//blockquote
var blockquote = result.find("blockquote");
blockquote.each(function (key, value) {
var html = $(value).html();
html = html.replace(coloregex, '<span class="color" data-color="$1"></span>');
html = html.replace(nameandtimeregex, '<small><i class="fa fa-user"></i> $1 <i class="fa fa-clock-o"></i> $2</small>');
html = html.replace(nameregex, '<small><i class="fa fa-user"></i> $1</small>');
html = html.replace(timeregex, '<small><i class="fa fa-clock-o"></i> $1</small>');
$(value).html(html);
});
var blockquotecolor = result.find("blockquote .color");
blockquotecolor.each(function (key, value) {
$(value).closest("blockquote").css('border-left-color', $(value).attr('data-color'));
});
return result;
}
//jQuery sortByDepth
$.fn.sortByDepth = function () {
var ar = this.map(function () {
return {
length: $(this).parents().length,
elt: this
}
}).get(),
result = [],
i = ar.length;
ar.sort(function (a, b) {
return a.length - b.length;
});
while (i--) {
result.push(ar[i].elt);
}
return $(result);
};
//remove hash
function removeHash() {
history.pushState("", document.title, window.location.pathname + window.location.search);
}
//toc
function generateToc(id) {
var target = $('#' + id);
target.html('');
new Toc('doc', {
'level': 3,
'top': -1,
'class': 'toc',
'targetId': id
});
if(target.text() == 'undefined')
target.html('');
var backtotop = $('<a class="back-to-top" href="#">Back to top</a>');
var gotobottom = $('<a class="go-to-bottom" href="#">Go to bottom</a>');
backtotop.click(function (e) {
e.preventDefault();
e.stopPropagation();
if (scrollToTop)
scrollToTop();
removeHash();
});
gotobottom.click(function (e) {
e.preventDefault();
e.stopPropagation();
if (scrollToBottom)
scrollToBottom();
removeHash();
});
target.append(backtotop).append(gotobottom);
}
//smooth all hash trigger scrolling
function smoothHashScroll() {
var hashElements = $("a[href^='#']:not([smoothhashscroll])").toArray();
for (var i = 0; i < hashElements.length; i++) {
var element = hashElements[i];
var $element = $(element);
var hash = element.hash;
if (hash) {
$element.on('click', function (e) {
// store hash
var hash = this.hash;
if ($(hash).length <= 0) return;
// prevent default anchor click behavior
e.preventDefault();
// animate
$('html, body').animate({
scrollTop: $(hash).offset().top
}, 100, "linear", function () {
// when done, add hash to url
// (default click behaviour)
window.location.hash = hash;
});
});
$element.attr('smoothhashscroll', '');
}
}
}
function setSizebyAttr(element, target) {
var width = $(element).attr("width") ? $(element).attr("width") : '100%';
var height = $(element).attr("height") ? $(element).attr("height") : '360px';
@ -168,10 +287,10 @@ function imgPlayiframe(element, src) {
var anchorForId = function (id) {
var anchor = document.createElement("a");
anchor.className = "header-link";
anchor.className = "header-link hidden-xs";
anchor.href = "#" + id;
anchor.innerHTML = "<span class=\"sr-only\">Permalink</span><i class=\"fa fa-link\"></i>";
anchor.title = "Permalink";
anchor.innerHTML = "<span class=\"sr-only\"></span><i class=\"fa fa-link\"></i>";
anchor.title = id;
return anchor;
};
@ -179,12 +298,14 @@ var linkifyAnchors = function (level, containingElement) {
var headers = containingElement.getElementsByTagName("h" + level);
for (var h = 0; h < headers.length; h++) {
var header = headers[h];
if (typeof header.id == "undefined" || header.id == "") {
var id = S(header.innerHTML.toLowerCase()).trim().stripTags().dasherize().s;
header.id = encodeURIComponent(id);
if (header.getElementsByClassName("header-link").length == 0) {
if (typeof header.id == "undefined" || header.id == "") {
//to escape characters not allow in css and humanize
var id = slugifyWithUTF8(header.innerHTML);
header.id = id;
}
header.appendChild(anchorForId(header.id));
}
header.appendChild(anchorForId(header.id));
}
};
@ -207,10 +328,10 @@ function scrollToHash() {
function highlightRender(code, lang) {
if (!lang || /no(-?)highlight|plain|text/.test(lang))
return;
if(lang == 'sequence') {
return '<div class="sequence-diagram">' + code + '</div>';
} else if(lang == 'flow') {
return '<div class="flow-chart">' + code + '</div>';
if (lang == 'sequence') {
return '<div class="sequence-diagram raw">' + code + '</div>';
} else if (lang == 'flow') {
return '<div class="flow-chart raw">' + code + '</div>';
}
var reallang = lang.replace('=', '');
var languages = hljs.listLanguages();
@ -238,10 +359,56 @@ emojify.setConfig({
var md = new Remarkable('full', {
html: true,
breaks: true,
langPrefix: "",
linkify: true,
typographer: true,
highlight: highlightRender
});
md.renderer.rules.list_item_open = function (/* tokens, idx, options, env */) {
return '<li class="raw">';
};
md.renderer.rules.blockquote_open = function (tokens, idx /*, options, env */ ) {
return '<blockquote class="raw">\n';
};
md.renderer.rules.hardbreak = function (tokens, idx, options /*, env */ ) {
return md.options.xhtmlOut ? '<br /><br />' : '<br><br>';
};
md.renderer.rules.fence = function (tokens, idx, options, env, self) {
var token = tokens[idx];
var langClass = '';
var langPrefix = options.langPrefix;
var langName = '',
fenceName;
var highlighted;
if (token.params) {
//
// ```foo bar
//
// Try custom renderer "foo" first. That will simplify overwrite
// for diagrams, latex, and any other fenced block with custom look
//
fenceName = token.params.split(/\s+/g)[0];
if (Remarkable.utils.has(self.rules.fence_custom, fenceName)) {
return self.rules.fence_custom[fenceName](tokens, idx, options, env, self);
}
langName = Remarkable.utils.escapeHtml(Remarkable.utils.replaceEntities(Remarkable.utils.unescapeMd(fenceName)));
langClass = ' class="' + langPrefix + langName.replace('=', '') + ' hljs"';
}
if (options.highlight) {
highlighted = options.highlight(token.content, langName) || Remarkable.utils.escapeHtml(token.content);
} else {
highlighted = Remarkable.utils.escapeHtml(token.content);
}
return '<pre><code' + langClass + '>' + highlighted + '</code></pre>' + md.renderer.getBreak(tokens, idx);
};
//youtube
var youtubePlugin = new Plugin(
// regexp to match
@ -251,7 +418,7 @@ var youtubePlugin = new Plugin(
function (match, utils) {
var videoid = match[1];
if (!videoid) return;
var div = $('<div class="youtube"></div>');
var div = $('<div class="youtube raw"></div>');
setSizebyAttr(div, div);
div.attr('videoid', videoid);
var icon = '<i class="icon fa fa-youtube-play fa-5x"></i>';
@ -270,7 +437,7 @@ var vimeoPlugin = new Plugin(
function (match, utils) {
var videoid = match[1];
if (!videoid) return;
var div = $('<div class="vimeo"></div>');
var div = $('<div class="vimeo raw"></div>');
setSizebyAttr(div, div);
div.attr('videoid', videoid);
var icon = '<i class="icon fa fa-vimeo-square fa-5x"></i>';
@ -298,7 +465,7 @@ var mathjaxPlugin = new Plugin(
// this function will be called when something matches
function (match, utils) {
//var code = $(match).text();
return '<span class="mathjax">' + match[0] + '</span>';
return '<span class="mathjax raw">' + match[0] + '</span>';
}
);
md.use(youtubePlugin);

View file

@ -1,8 +0,0 @@
(function (d, s, id) {
var js, fjs = d.getElementsByTagName(s)[0];
if (d.getElementById(id)) return;
js = d.createElement(s);
js.id = id;
js.src = "//connect.facebook.net/en_US/sdk.js#xfbml=1&version=v2.3&appId=1436904003272070";
fjs.parentNode.insertBefore(js, fjs);
}(document, 'script', 'facebook-jssdk'));

View file

@ -319,8 +319,9 @@ function parseToHistory(list, notehistory, callback) {
else if (notehistory && notehistory.length > 0) {
for (var i = 0; i < notehistory.length; i++) {
//parse time to timestamp and fromNow
notehistory[i].timestamp = moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a').unix();
notehistory[i].timestamp = moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a').valueOf();
notehistory[i].fromNow = moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a').fromNow();
notehistory[i].time = moment(notehistory[i].time, 'MMMM Do YYYY, h:mm:ss a').format('llll');
if (list.get('id', notehistory[i].id).length == 0)
list.add(notehistory[i]);
}

View file

@ -1,16 +1,21 @@
//constant vars
//settings
var debug = false;
var version = '0.2.9';
var version = '0.3.1';
var defaultTextHeight = 18;
var viewportMargin = 20;
var defaultExtraKeys = {
"Cmd-S": function () {
return CodeMirror.PASS
},
"Ctrl-S": function () {
return CodeMirror.PASS
},
"Enter": "newlineAndIndentContinueMarkdownList"
};
var idleTime = 300000; //5 mins
var doneTypingDelay = 400;
var finishChangeDelay = 400;
var cursorActivityDelay = 50;
var cursorAnimatePeriod = 100;
@ -97,12 +102,27 @@ var supportExternals = [
search: 'gist'
}
];
var supportGenerals = [
var supportBlockquoteTags = [
{
text: '[name tag]',
search: '[]',
command: function () {
return moment().format('llll');
return '[name=' + personalInfo.name + ']';
},
search: 'time'
},
{
text: '[time tag]',
search: '[]',
command: function () {
return '[time=' + moment().format('llll') + ']';
},
},
{
text: '[color tag]',
search: '[]',
command: function () {
return '[color=' + personalInfo.color + ']';
}
}
];
var modeType = {
@ -131,6 +151,7 @@ var defaultMode = modeType.both;
//global vars
var loaded = false;
var needRefresh = false;
var isDirty = false;
var editShown = false;
var visibleXS = false;
@ -192,7 +213,7 @@ var editor = CodeMirror.fromTextArea(textit, {
extraKeys: defaultExtraKeys,
readOnly: true
});
inlineAttachment.editors.codemirror4.attach(editor);
var inlineAttach = inlineAttachment.editors.codemirror4.attach(editor);
defaultTextHeight = parseInt($(".CodeMirror").css('line-height'));
//ui vars
@ -203,7 +224,7 @@ var ui = {
shortStatus: $(".ui-short-status"),
status: $(".ui-status"),
new: $(".ui-new"),
pretty: $(".ui-pretty"),
share: $(".ui-share"),
download: {
markdown: $(".ui-download-markdown")
},
@ -217,7 +238,24 @@ var ui = {
mode: $(".ui-mode"),
edit: $(".ui-edit"),
view: $(".ui-view"),
both: $(".ui-both")
both: $(".ui-both"),
uploadImage: $(".ui-upload-image")
},
infobar: {
lastchange: $(".ui-lastchange"),
permission: {
permission: $(".ui-permission"),
label: $(".ui-permission-label"),
freely: $(".ui-permission-freely"),
editable: $(".ui-permission-editable"),
locked: $(".ui-permission-locked")
}
},
toc: {
toc: $('.ui-toc'),
affix: $('.ui-affix-toc'),
label: $('.ui-toc-label'),
dropdown: $('.ui-toc-dropdown')
},
area: {
edit: $(".ui-edit-area"),
@ -263,10 +301,16 @@ function idleStateChange() {
updateOnlineStatus();
}
loginStateChangeEvent = function () {
location.reload(true);
function setNeedRefresh() {
$('#refreshModal').modal('show');
needRefresh = true;
editor.setOption('readOnly', true);
socket.disconnect();
showStatus(statusType.offline);
}
loginStateChangeEvent = setNeedRefresh;
//visibility
var wasFocus = false;
Visibility.change(function (e, state) {
@ -315,6 +359,11 @@ $(document).ready(function () {
$body.removeClass('fixfixed');
});
}
//showup
$().showUp('.navbar', {
upClass: 'navbar-hide',
downClass: 'navbar-show'
});
});
//when page resize
var windowResizeDelay = 200;
@ -327,12 +376,38 @@ $(window).resize(function () {
});
//when page unload
$(window).unload(function () {
emitRefresh();
emitUpdate();
});
//when page hash change
window.onhashchange = locationHashChanged;
function locationHashChanged(e) {
e.stopPropagation();
e.preventDefault();
if (currentMode != modeType.both) {
return;
}
var hashtarget = $("[id$='" + location.hash.substr(1) + "']");
if (hashtarget.length > 0) {
var linenumber = hashtarget.attr('data-startline');
if (linenumber) {
editor.setOption('viewportMargin', Infinity);
editor.setOption('viewportMargin', viewportMargin);
var t = editor.charCoords({
line: linenumber,
ch: 0
}, "local").top;
editor.scrollTo(null, t - defaultTextHeight * 1.2);
}
}
}
function windowResize() {
checkResponsive();
checkEditorStyle();
checkTocStyle();
//refresh editor
if (loaded) {
editor.setOption('viewportMargin', Infinity);
setTimeout(function () {
@ -373,6 +448,39 @@ function checkEditorStyle() {
}
}
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');
} 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 && !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;
@ -461,12 +569,21 @@ function changeMode(type) {
}
if (currentMode != modeType.view && visibleLG) {
//editor.focus();
editor.refresh();
//editor.refresh();
} else {
editor.getInputField().blur();
}
if (changeMode != modeType.edit)
if (currentMode == modeType.edit || currentMode == modeType.both) {
ui.toolbar.uploadImage.fadeIn();
} 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'));
}
restoreInfo();
windowResize();
@ -489,10 +606,9 @@ function changeMode(type) {
}
//button actions
var noteId = window.location.pathname.split('/')[1];
var url = window.location.origin + '/' + noteId;
//pretty
ui.toolbar.pretty.attr("href", url + "/pretty");
var url = window.location.pathname;
//share
ui.toolbar.share.attr("href", url + "/share");
//download
//markdown
ui.toolbar.download.markdown.click(function () {
@ -534,6 +650,73 @@ ui.toolbar.import.dropbox.click(function () {
ui.toolbar.import.clipboard.click(function () {
//na
});
//upload image
ui.toolbar.uploadImage.bind('change', function (e) {
var files = e.target.files || e.dataTransfer.files;
e.dataTransfer = {};
e.dataTransfer.files = files;
inlineAttach.onDrop(e);
});
//toc
ui.toc.dropdown.click(function (e) {
e.stopPropagation();
});
function scrollToTop() {
if (currentMode == modeType.both) {
if (editor.getScrollInfo().top != 0)
editor.scrollTo(0, 0);
else
ui.area.view.animate({
scrollTop: 0
}, 100, "linear");
} else {
$(document.body).animate({
scrollTop: 0
}, 100, "linear");
}
}
function scrollToBottom() {
if (currentMode == modeType.both) {
var scrollInfo = editor.getScrollInfo();
var scrollHeight = scrollInfo.height;
if (scrollInfo.top != scrollHeight)
editor.scrollTo(0, scrollHeight * 2);
else
ui.area.view.animate({
scrollTop: ui.area.view[0].scrollHeight
}, 100, "linear");
} else {
$(document.body).animate({
scrollTop: $(document.body)[0].scrollHeight
}, 100, "linear");
}
}
var enoughForAffixToc = true;
//scrollspy
function generateScrollspy() {
$(document.body).scrollspy({
target: '.scrollspy-body'
});
ui.area.view.scrollspy({
target: '.scrollspy-view'
});
$(document.body).scrollspy('refresh');
ui.area.view.scrollspy('refresh');
if (enoughForAffixToc) {
ui.toc.toc.hide();
ui.toc.affix.show();
} else {
ui.toc.affix.hide();
ui.toc.toc.show();
}
$(document.body).scroll();
ui.area.view.scroll();
}
//fix for wrong autofocus
$('#clipboardModal').on('shown.bs.modal', function () {
$('#clipboardModal').blur();
@ -549,6 +732,9 @@ $("#clipboardModalConfirm").click(function () {
$("#clipboardModalContent").html('');
}
});
$('#refreshModalRefresh').click(function () {
location.reload(true);
});
function parseToEditor(data) {
var parsed = toMarkdown(data);
@ -585,19 +771,20 @@ function importFromUrl(url) {
}
function isValidURL(str) {
var 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;
}
var 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;
}
//mode
}
//mode
ui.toolbar.mode.click(function () {
toggleMode();
});
@ -613,18 +800,63 @@ ui.toolbar.view.click(function () {
ui.toolbar.both.click(function () {
changeMode(modeType.both);
});
//permission
//freely
ui.infobar.permission.freely.click(function () {
updatePermission("freely");
});
//editable
ui.infobar.permission.editable.click(function () {
updatePermission("editable");
});
//locked
ui.infobar.permission.locked.click(function () {
updatePermission("locked");
});
function updatePermission(_permission) {
if (_permission != permission) {
socket.emit('permission', _permission);
}
}
function checkPermission() {
var label = null;
var title = null;
switch (permission) {
case "freely":
label = '<i class="fa fa-leaf"></i> Freely';
title = "Anyone can edit";
break;
case "editable":
label = '<i class="fa fa-pencil"></i> Editable';
title = "Signed people can edit";
break;
case "locked":
label = '<i class="fa fa-lock"></i> Locked';
title = "Only owner can edit";
break;
}
if (personalInfo.userid == owner) {
label += ' <i class="fa fa-caret-down"></i>';
ui.infobar.permission.label.removeClass('disabled');
} else {
ui.infobar.permission.label.addClass('disabled');
}
ui.infobar.permission.label.html(label).attr('title', title);
}
//socket.io actions
var socket = io.connect();
//overwrite original event for checking login state
var on = socket.on;
socket.on = function () {
if (!checkLoginStateChanged())
if (!checkLoginStateChanged() && !needRefresh)
on.apply(socket, arguments);
};
var emit = socket.emit;
socket.emit = function () {
if (!checkLoginStateChanged())
if (!checkLoginStateChanged() && !needRefresh)
emit.apply(socket, arguments);
};
socket.on('info', function (data) {
@ -653,17 +885,53 @@ socket.on('connect', function (data) {
});
socket.on('version', function (data) {
if (data != version)
location.reload(true);
setNeedRefresh();
});
socket.on('check', function (data) {
if (data.id == socket.id) {
lastchangetime = data.updatetime;
lastchangeui = ui.infobar.lastchange;
updateLastChange();
return;
}
var currentHash = md5(LZString.compressToUTF16(editor.getValue()));
var hashMismatch = (currentHash != data.hash);
if (hashMismatch)
socket.emit('refresh');
else {
lastchangetime = data.updatetime;
lastchangeui = ui.infobar.lastchange;
updateLastChange();
}
});
socket.on('permission', function (data) {
permission = data.permission;
checkPermission();
});
var otk = null;
var owner = null;
var permission = null;
socket.on('refresh', function (data) {
var currentHash = md5(LZString.compressToUTF16(editor.getValue()));
var hashMismatch = (currentHash != data.hash);
saveInfo();
var body = data.body;
body = LZString.decompressFromUTF16(body);
if (body)
editor.setValue(body);
else
editor.setValue("");
otk = data.otk;
owner = data.owner;
permission = data.permission;
if (hashMismatch) {
var body = data.body;
body = LZString.decompressFromUTF16(body);
if (body)
editor.setValue(body);
else
editor.setValue("");
}
lastchangetime = data.updatetime;
lastchangeui = ui.infobar.lastchange;
updateLastChange();
if (!loaded) {
editor.clearHistory();
@ -673,9 +941,19 @@ socket.on('refresh', function (data) {
loaded = true;
emitUserStatus(); //send first user status
updateOnlineStatus(); //update first online status
setTimeout(function () {
//work around editor not refresh
editor.refresh();
//work around cursor not refresh
for (var i = 0; i < onlineUsers.length; i++) {
buildCursor(onlineUsers[i]);
}
//work around might not scroll to hash
scrollToHash();
}, 1);
} else {
//if current doc is equal to the doc before disconnect
if (LZString.compressToUTF16(editor.getValue()) !== data.body)
if (hashMismatch)
editor.clearHistory();
else {
if (lastInfo.history)
@ -684,21 +962,58 @@ socket.on('refresh', function (data) {
lastInfo.history = null;
}
updateView();
if (hashMismatch)
updateView();
if (editor.getOption('readOnly'))
editor.setOption('readOnly', false);
restoreInfo();
checkPermission();
});
var changeStack = [];
var changeBusy = false;
socket.on('change', function (data) {
data = LZString.decompressFromUTF16(data);
data = JSON.parse(data);
editor.replaceRange(data.text, data.from, data.to, "ignoreHistory");
isDirty = true;
clearTimeout(finishChangeTimer);
finishChangeTimer = setTimeout(finishChange, finishChangeDelay);
changeStack.push(data);
if (!changeBusy)
executeChange();
});
function executeChange() {
if (changeStack.length > 0) {
changeBusy = true;
var data = changeStack.shift();
if (data.otk != otk) {
var found = false;
for (var i = 0, l = changeStack.length; i < l; i++) {
if (changeStack[i].otk == otk) {
changeStack.unshift(data);
data = changeStack[i];
found = true;
break;
}
}
if (!found) {
socket.emit('refresh');
changeBusy = false;
return;
}
}
otk = data.nextotk;
if (data.id == personalInfo.id)
editor.replaceRange(data.text, data.from, data.to, 'self::' + data.origin);
else
editor.replaceRange(data.text, data.from, data.to, "ignoreHistory");
executeChange();
} else {
changeBusy = false;
}
}
socket.on('online users', function (data) {
data = LZString.decompressFromUTF16(data);
data = JSON.parse(data);
@ -795,7 +1110,7 @@ var onlineUserList = new List('online-user-list', options);
var shortOnlineUserList = new List('short-online-user-list', options);
function updateOnlineStatus() {
if (!loaded) return;
if (!loaded || !socket.connected) return;
var _onlineUsers = deduplicateOnlineUsers(onlineUsers);
showStatus(statusType.online, _onlineUsers.length);
var items = onlineUserList.items;
@ -851,7 +1166,7 @@ function sortOnlineUserList(list) {
var userbIsSelf = (userb.id == personalInfo.id || (userb.login && userb.userid == personalInfo.userid));
if (useraIsSelf && !userbIsSelf) {
return -1;
} else if(!useraIsSelf && userbIsSelf) {
} else if (!useraIsSelf && userbIsSelf) {
return 1;
} else {
if (usera.login && !userb.login)
@ -975,7 +1290,7 @@ function checkCursorTag(coord, ele) {
var offsetTop = defaultTextHeight;
if (width > 0 && height > 0) {
if (left + width + offsetLeft > editorWidth - curosrtagMargin) {
offsetLeft = -(width + 4);
offsetLeft = -(width + 10);
}
if (top + height + offsetTop > Math.max(viewportHeight, editorHeight) + curosrtagMargin && top - height > curosrtagMargin) {
offsetTop = -(height);
@ -1124,16 +1439,61 @@ function buildCursor(user) {
editor.on('beforeChange', function (cm, change) {
if (debug)
console.debug(change);
var self = change.origin.split('self::');
if (self.length == 2) {
change.origin = self[1];
self = true;
} else {
self = false;
}
if (self) {
change.canceled = true;
} else {
var isIgnoreEmitEvent = (ignoreEmitEvents.indexOf(change.origin) != -1);
if (!isIgnoreEmitEvent) {
switch (permission) {
case "freely":
//na
break;
case "editable":
if (!personalInfo.login) {
change.canceled = true;
$('.signin-modal').modal('show');
}
break;
case "locked":
if (personalInfo.userid != owner) {
change.canceled = true;
$('.locked-modal').modal('show');
}
break;
}
}
}
});
var ignoreEmitEvents = ['setValue', 'ignoreHistory'];
editor.on('change', function (i, op) {
if (debug)
console.debug(op);
if (op.origin != 'setValue' && op.origin != 'ignoreHistory') {
socket.emit('change', LZString.compressToUTF16(JSON.stringify(op)));
var isIgnoreEmitEvent = (ignoreEmitEvents.indexOf(op.origin) != -1);
if (!isIgnoreEmitEvent) {
var out = {
text: op.text,
from: op.from,
to: op.to,
origin: op.origin
};
socket.emit('change', LZString.compressToUTF16(JSON.stringify(out)));
}
isDirty = true;
clearTimeout(doneTypingTimer);
doneTypingTimer = setTimeout(doneTyping, doneTypingDelay);
clearTimeout(finishChangeTimer);
finishChangeTimer = setTimeout(function () {
if (!isIgnoreEmitEvent)
finishChange(true);
else
finishChange(false);
}, finishChangeDelay);
});
editor.on('focus', function (cm) {
for (var i = 0; i < onlineUsers.length; i++) {
@ -1236,22 +1596,17 @@ function restoreInfo() {
}
//view actions
var doneTypingTimer = null;
var finishChangeTimer = null;
var input = editor.getInputField();
//user is "finished typing," do something
function doneTyping() {
emitRefresh();
function finishChange(emit) {
if (emit)
emitUpdate();
updateView();
}
function finishChange() {
updateView();
}
function emitRefresh() {
function emitUpdate() {
var value = editor.getValue();
socket.emit('refresh', LZString.compressToUTF16(value));
socket.emit('update', LZString.compressToUTF16(value));
}
var lastResult = null;
@ -1267,6 +1622,11 @@ function updateView() {
updateDataAttrs(result, ui.area.markdown.children().toArray());
lastResult = $(result).clone();
finishView(ui.area.view);
autoLinkify(ui.area.view);
generateToc('toc');
generateToc('toc-affix');
generateScrollspy();
smoothHashScroll();
writeHistory(ui.area.markdown);
isDirty = false;
clearMap();
@ -1535,7 +1895,7 @@ $(editor.getInputField())
return '$1```' + lang + '=\n\n```';
},
done: function () {
editor.doc.cm.moveV(-1, "line");
editor.doc.cm.execCommand("goLineUp");
},
context: function () {
return isInCode;
@ -1555,6 +1915,28 @@ $(editor.getInputField())
context: function (text) {
return !isInCode;
}
},
{ //blockquote personal info & general info
match: /(?:^|\n|\s)(\>.*)(\[\])(\w*)$/,
search: function (term, callback) {
var list = [];
$.map(supportBlockquoteTags, function (blockquotetag) {
if (blockquotetag.search.indexOf(term) === 0)
list.push(blockquotetag.command());
});
$.map(supportReferrals, function (referral) {
if (referral.search.indexOf(term) === 0)
list.push(referral.text);
})
callback(list);
checkCursorMenu();
},
replace: function (value) {
return '$1' + value;
},
context: function (text) {
return !isInCode;
}
},
{ //referral
match: /(^|\n|\s)(\!|\!|\[\])(\w*)$/,
@ -1585,41 +1967,6 @@ $(editor.getInputField())
context: function (text) {
return !isInCode;
}
},
{ //blockquote personal info & general info
match: /(^|\n|\s|\>.*)\[(\w*)=$/,
search: function (term, callback) {
var list = typeof personalInfo[term] != 'undefined' ? [personalInfo[term]] : [];
$.map(supportGenerals, function (general) {
if (general.search.indexOf(term) === 0)
list.push(general.command());
});
callback(list);
checkCursorMenu();
},
replace: function (value) {
return '$1[$2=' + value;
},
context: function (text) {
return !isInCode;
}
},
{ //blockquote quick start tag
match: /(^.*(?!>)\n|)(\>\s{0,1})$/,
search: function (term, callback) {
var self = '[name=' + personalInfo.name + '] [time=' + moment().format('llll') + '] [color=' + personalInfo.color + ']';
callback([self]);
checkCursorMenu();
},
template: function (value) {
return '[Your name, time, color tags]';
},
replace: function (value) {
return '$1$2' + value;
},
context: function (text) {
return !isInCode;
}
}
], {
appendTo: $('.cursor-menu')

View file

@ -1,9 +1,81 @@
var raw = $(".markdown-body").text();
var markdown = LZString.decompressFromBase64(raw);
var result = postProcess(md.render(markdown));
var markdown = $(".markdown-body");
markdown.html(result);
markdown.show();
var text = $('<textarea/>').html(markdown.html()).text();
var result = postProcess(md.render(text));
markdown.html(result.html());
$(document.body).show();
finishView(markdown);
autoLinkify(markdown);
scrollToHash();
generateToc('toc');
generateToc('toc-affix');
smoothHashScroll();
lastchangetime = $('.ui-lastchange').text();
lastchangeui = $('.ui-lastchange');
updateLastChange();
var url = window.location.pathname;
$('.ui-edit').attr('href', url + '/edit');
var toc = $('.ui-toc');
var tocAffix = $('.ui-affix-toc');
var tocDropdown = $('.ui-toc-dropdown');
//toc
tocDropdown.click(function (e) {
e.stopPropagation();
});
var enoughForAffixToc = true;
function generateScrollspy() {
$(document.body).scrollspy({
target: ''
});
$(document.body).scrollspy('refresh');
if (enoughForAffixToc) {
toc.hide();
tocAffix.show();
} else {
tocAffix.hide();
toc.show();
}
$(document.body).scroll();
}
function windowResize() {
//toc right
var paddingRight = parseFloat(markdown.css('padding-right'));
var right = ($(window).width() - (markdown.offset().left + markdown.outerWidth() - paddingRight));
toc.css('right', right + 'px');
//affix toc left
var newbool;
var rightMargin = (markdown.parent().outerWidth() - markdown.outerWidth()) / 2;
//for ipad or wider device
if (rightMargin >= 133) {
newbool = true;
var affixLeftMargin = (tocAffix.outerWidth() - tocAffix.width()) / 2;
var left = markdown.offset().left + markdown.outerWidth() - affixLeftMargin;
tocAffix.css('left', left + 'px');
} else {
newbool = false;
}
if (newbool != enoughForAffixToc) {
enoughForAffixToc = newbool;
generateScrollspy();
}
}
$(window).resize(function () {
windowResize();
});
$(document).ready(function () {
windowResize();
generateScrollspy();
});
function scrollToTop() {
$(document.body).animate({
scrollTop: 0
}, 100, "linear");
}
function scrollToBottom() {
$(document.body).animate({
scrollTop: $(document.body)[0].scrollHeight
}, 100, "linear");
}

View file

@ -8,7 +8,7 @@ md.renderer.rules.blockquote_open = function (tokens, idx /*, options, env */ )
if (tokens[idx].lines && tokens[idx].level === 0) {
var startline = tokens[idx].lines[0] + 1;
var endline = tokens[idx].lines[1];
return '<blockquote class="part" data-startline="' + startline + '" data-endline="' + endline + '">\n';
return '<blockquote class="raw part" data-startline="' + startline + '" data-endline="' + endline + '">\n';
}
return '<blockquote>\n';
};
@ -55,9 +55,9 @@ md.renderer.rules.paragraph_open = function (tokens, idx) {
if (tokens[idx].lines && tokens[idx].level === 0) {
var startline = tokens[idx].lines[0] + 1;
var endline = tokens[idx].lines[1];
return '<p class="part" data-startline="' + startline + '" data-endline="' + endline + '">';
return tokens[idx].tight ? '' : '<p class="part" data-startline="' + startline + '" data-endline="' + endline + '">';
}
return '';
return tokens[idx].tight ? '' : '<p>';
};
md.renderer.rules.heading_open = function (tokens, idx) {
@ -106,7 +106,7 @@ md.renderer.rules.fence = function (tokens, idx, options, env, self) {
}
langName = Remarkable.utils.escapeHtml(Remarkable.utils.replaceEntities(Remarkable.utils.unescapeMd(fenceName)));
langClass = ' class="' + langPrefix + langName + '"';
langClass = ' class="' + langPrefix + langName.replace('=', '') + ' hljs"';
}
if (options.highlight) {
@ -193,7 +193,8 @@ function buildMapInner(syncBack) {
'line-height': textarea.css('line-height'),
'word-wrap': wrap.css('word-wrap'),
'white-space': wrap.css('white-space'),
'word-break': wrap.css('word-break')
'word-break': wrap.css('word-break'),
'tab-size': '38px'
}).appendTo('body');
offset = ui.area.view.scrollTop() - ui.area.view.offset().top;
@ -313,7 +314,7 @@ function syncScrollToView(event, _lineNo) {
var textHeight = editor.defaultTextHeight();
lineNo = Math.floor(scrollInfo.top / textHeight);
//if reach bottom, then scroll to end
if (scrollInfo.top + scrollInfo.clientHeight >= scrollInfo.height - defaultTextHeight) {
if (scrollInfo.height > scrollInfo.clientHeight && scrollInfo.top + scrollInfo.clientHeight >= scrollInfo.height - defaultTextHeight) {
posTo = ui.area.view[0].scrollHeight - ui.area.view.height();
} else {
topDiffPercent = (scrollInfo.top % textHeight) / textHeight;

View file

@ -57,7 +57,7 @@
cm.operation(function() {
for (var i = ranges.length - 1; i >= 0; i--)
cm.replaceRange(inserts[i], ranges[i].from(), ranges[i].to(), "+insert");
cm.replaceRange(inserts[i], ranges[i].from(), ranges[i].to(), "+input");
});
}

File diff suppressed because one or more lines are too long

View file

@ -1318,11 +1318,13 @@
minimal = hasCopyEvent &&
(range.to().line - range.from().line > 100 || (selected = cm.getSelection()).length > 1000);
var content = minimal ? "-" : selected || cm.getSelection();
this.textarea.value = content;
if(!this.composing)
this.textarea.value = content;
if (cm.state.focused) selectInput(this.textarea);
if (ie && ie_version >= 9) this.hasSelection = content;
} else if (!typing) {
this.prevInput = this.textarea.value = "";
if(!this.composing)
this.prevInput = this.textarea.value = "";
if (ie && ie_version >= 9) this.hasSelection = null;
}
this.inaccurateSelection = minimal;

View file

@ -0,0 +1 @@
.hljs{display:block;background:white;padding:0.5em;color:#333333;overflow-x:auto;-webkit-text-size-adjust:none}.hljs-comment,.bash .hljs-shebang,.java .hljs-javadoc,.javascript .hljs-javadoc{color:#969896}.hljs-string,.apache .hljs-sqbracket,.coffeescript .hljs-subst,.coffeescript .hljs-regexp,.cpp .hljs-preprocessor,.c .hljs-preprocessor,.javascript .hljs-regexp,.json .hljs-attribute,.makefile .hljs-variable,.markdown .hljs-value,.markdown .hljs-link_label,.markdown .hljs-strong,.markdown .hljs-emphasis,.markdown .hljs-blockquote,.nginx .hljs-regexp,.nginx .hljs-number,.objectivec .hljs-preprocessor .hljs-title,.perl .hljs-regexp,.php .hljs-regexp,.xml .hljs-value,.less .hljs-built_in,.scss .hljs-built_in{color:#df5000}.hljs-keyword,.css .hljs-at_rule,.css .hljs-important,.http .hljs-request,.ini .hljs-setting,.java .hljs-javadoctag,.javascript .hljs-tag,.javascript .hljs-javadoctag,.nginx .hljs-title,.objectivec .hljs-preprocessor,.php .hljs-phpdoc,.sql .hljs-built_in,.less .hljs-tag,.less .hljs-at_rule,.scss .hljs-tag,.scss .hljs-at_rule,.scss .hljs-important,.stylus .hljs-at_rule,.go .hljs-typename,.swift .hljs-preprocessor{color:#a71d5d}.apache .hljs-common,.apache .hljs-cbracket,.apache .hljs-keyword,.bash .hljs-literal,.bash .hljs-built_in,.coffeescript .hljs-literal,.coffeescript .hljs-built_in,.coffeescript .hljs-number,.cpp .hljs-number,.cpp .hljs-built_in,.c .hljs-number,.c .hljs-built_in,.cs .hljs-number,.cs .hljs-built_in,.css .hljs-attribute,.css .hljs-hexcolor,.css .hljs-number,.css .hljs-function,.http .hljs-literal,.http .hljs-attribute,.java .hljs-number,.javascript .hljs-built_in,.javascript .hljs-literal,.javascript .hljs-number,.json .hljs-number,.makefile .hljs-keyword,.markdown .hljs-link_reference,.nginx .hljs-built_in,.objectivec .hljs-literal,.objectivec .hljs-number,.objectivec .hljs-built_in,.php .hljs-literal,.php .hljs-number,.python .hljs-number,.ruby .hljs-prompt,.ruby .hljs-constant,.ruby .hljs-number,.ruby .hljs-subst .hljs-keyword,.ruby .hljs-symbol,.sql .hljs-number,.puppet .hljs-function,.less .hljs-number,.less .hljs-hexcolor,.less .hljs-function,.less .hljs-attribute,.scss .hljs-preprocessor,.scss .hljs-number,.scss .hljs-hexcolor,.scss .hljs-function,.scss .hljs-attribute,.stylus .hljs-number,.stylus .hljs-hexcolor,.stylus .hljs-attribute,.stylus .hljs-params,.go .hljs-built_in,.go .hljs-constant,.swift .hljs-built_in,.swift .hljs-number{color:#0086b3}.apache .hljs-tag,.cs .hljs-xmlDocTag,.css .hljs-tag,.xml .hljs-title,.stylus .hljs-tag{color:#63a35c}.bash .hljs-variable,.cs .hljs-preprocessor,.cs .hljs-preprocessor .hljs-keyword,.css .hljs-attr_selector,.css .hljs-value,.ini .hljs-value,.ini .hljs-keyword,.javascript .hljs-tag .hljs-title,.makefile .hljs-constant,.nginx .hljs-variable,.xml .hljs-tag,.scss .hljs-variable{color:#333333}.bash .hljs-title,.coffeescript .hljs-title,.cpp .hljs-title,.c .hljs-title,.cs .hljs-title,.css .hljs-id,.css .hljs-class,.css .hljs-pseudo,.ini .hljs-title,.java .hljs-title,.javascript .hljs-title,.makefile .hljs-title,.objectivec .hljs-title,.perl .hljs-sub,.php .hljs-title,.python .hljs-decorator,.python .hljs-title,.ruby .hljs-parent,.ruby .hljs-title,.xml .hljs-attribute,.puppet .hljs-title,.less .hljs-id,.less .hljs-pseudo,.less .hljs-class,.scss .hljs-id,.scss .hljs-pseudo,.scss .hljs-class,.stylus .hljs-class,.stylus .hljs-id,.stylus .hljs-pseudo,.stylus .hljs-title,.swift .hljs-title,.diff .hljs-chunk{color:#795da3}.coffeescript .hljs-reserved,.coffeescript .hljs-attribute{color:#1d3e81}.diff .hljs-chunk{font-weight:bold}.diff .hljs-addition{color:#55a532;background-color:#eaffea}.diff .hljs-deletion{color:#bd2c00;background-color:#ffecec}.markdown .hljs-link_url{text-decoration:underline}

View file

@ -1 +1 @@
.hljs{display:block;overflow-x:auto;padding:0.5em;color:#333;background:#f8f8f8;-webkit-text-size-adjust:none}.hljs-comment,.diff .hljs-header,.hljs-javadoc{color:#998;font-style:italic}.hljs-keyword,.css .rule .hljs-keyword,.hljs-winutils,.nginx .hljs-title,.hljs-subst,.hljs-request,.hljs-status{color:#333;font-weight:bold}.hljs-number,.hljs-hexcolor,.ruby .hljs-constant{color:#008080}.hljs-string,.hljs-tag .hljs-value,.hljs-phpdoc,.hljs-dartdoc,.tex .hljs-formula{color:#d14}.hljs-title,.hljs-id,.scss .hljs-preprocessor{color:#900;font-weight:bold}.hljs-list .hljs-keyword,.hljs-subst{font-weight:normal}.hljs-class .hljs-title,.hljs-type,.vhdl .hljs-literal,.tex .hljs-command{color:#458;font-weight:bold}.hljs-tag,.hljs-tag .hljs-title,.hljs-rules .hljs-property,.django .hljs-tag .hljs-keyword{color:#000080;font-weight:normal}.hljs-attribute,.hljs-variable,.lisp .hljs-body{color:#008080}.hljs-regexp{color:#009926}.hljs-symbol,.ruby .hljs-symbol .hljs-string,.lisp .hljs-keyword,.clojure .hljs-keyword,.scheme .hljs-keyword,.tex .hljs-special,.hljs-prompt{color:#990073}.hljs-built_in{color:#0086b3}.hljs-preprocessor,.hljs-pragma,.hljs-pi,.hljs-doctype,.hljs-shebang,.hljs-cdata{color:#999;font-weight:bold}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.diff .hljs-change{background:#0086b3}.hljs-chunk{color:#aaa}
.hljs{display:block;overflow-x:auto;padding:0.5em;color:#333;background:#f8f8f8;-webkit-text-size-adjust:none}.hljs-comment,.diff .hljs-header{color:#998;font-style:italic}.hljs-keyword,.css .rule .hljs-keyword,.hljs-winutils,.nginx .hljs-title,.hljs-subst,.hljs-request,.hljs-status{color:#333;font-weight:bold}.hljs-number,.hljs-hexcolor,.ruby .hljs-constant{color:#008080}.hljs-string,.hljs-tag .hljs-value,.hljs-doctag,.tex .hljs-formula{color:#d14}.hljs-title,.hljs-id,.scss .hljs-preprocessor{color:#900;font-weight:bold}.hljs-list .hljs-keyword,.hljs-subst{font-weight:normal}.hljs-class .hljs-title,.hljs-type,.vhdl .hljs-literal,.tex .hljs-command{color:#458;font-weight:bold}.hljs-tag,.hljs-tag .hljs-title,.hljs-rule .hljs-property,.django .hljs-tag .hljs-keyword{color:#000080;font-weight:normal}.hljs-attribute,.hljs-variable,.lisp .hljs-body,.hljs-name{color:#008080}.hljs-regexp{color:#009926}.hljs-symbol,.ruby .hljs-symbol .hljs-string,.lisp .hljs-keyword,.clojure .hljs-keyword,.scheme .hljs-keyword,.tex .hljs-special,.hljs-prompt{color:#990073}.hljs-built_in{color:#0086b3}.hljs-preprocessor,.hljs-pragma,.hljs-pi,.hljs-doctype,.hljs-shebang,.hljs-cdata{color:#999;font-weight:bold}.hljs-deletion{background:#fdd}.hljs-addition{background:#dfd}.diff .hljs-change{background:#0086b3}.hljs-chunk{color:#aaa}

File diff suppressed because one or more lines are too long

View file

@ -88,6 +88,8 @@
return false;
}
});
return inlineattach;
};
inlineAttachment.editors.codemirror4 = codeMirrorEditor4;

View file

@ -352,7 +352,7 @@
inlineAttachment.prototype.onFileInserted = function(file, id) {
if (this.settings.onFileReceived.call(this, file) !== false) {
this.lastValue = this.settings.progressText.replace(this.filenameTag, id);
this.editor.insertValue(this.lastValue);
this.editor.insertValue(this.lastValue + "\n");
}
};

View file

@ -250,9 +250,13 @@ if (typeof jQuery === 'undefined') {
if (context || context === '') {
if (isString(context)) { text = context; }
var cursor = editor.getCursor();
text = editor.getLine(cursor.line).slice(0, cursor.ch);
var match = text.match(strategy.match);
if (match) { return [strategy, match[strategy.index], match]; }
var line = editor.getLine(cursor.line);
var linematch = line.match(strategy.match);
if(linematch) {
text = line.slice(0, cursor.ch);
var textmatch = text.match(strategy.match);
if (textmatch) { return [strategy, textmatch[strategy.index], textmatch]; }
}
}
}
return []
@ -315,12 +319,14 @@ if (typeof jQuery === 'undefined') {
};
var dropdownViews = {};
$(document).on('click', function (e) {
/*
$(document).on('mousedown', function (e) {
var id = e.originalEvent && e.originalEvent.keepTextCompleteDropdown;
$.each(dropdownViews, function (key, view) {
if (key !== id) { view.deactivate(); }
});
});
*/
// Dropdown view
// =============
@ -335,6 +341,7 @@ if (typeof jQuery === 'undefined') {
this._data = []; // zipped data.
this.$inputEl = $(element);
this.option = option;
this.tap = false;
// Override setPosition method.
if (option.listPosition) { this.setPosition = option.listPosition; }
@ -499,7 +506,18 @@ if (typeof jQuery === 'undefined') {
// ---------------
_bindEvents: function () {
this.$el.on('touchstart.' + this.id, '.textcomplete-item', $.proxy(this._onClick, this));
this.$inputEl.on('blur', $.proxy(this.deactivate, this));
this.$el.on('touchstart.' + this.id, '.textcomplete-item', $.proxy(function(e) {
this.tap = true;
}, this));
this.$el.on('touchmove.' + this.id, '.textcomplete-item', $.proxy(function(e) {
this.tap = false;
}, this));
this.$el.on('touchend.' + this.id, '.textcomplete-item', $.proxy(function(e) {
if(e.cancelable && this.tap) {
this._onClick(e);
}
}, this));
this.$el.on('mousedown.' + this.id, '.textcomplete-item', $.proxy(this._onClick, this));
this.$el.on('mouseover.' + this.id, '.textcomplete-item', $.proxy(this._onMouseover, this));
this.$inputEl.on('keydown.' + this.id, $.proxy(this._onKeydown, this));
@ -819,6 +837,7 @@ if (typeof jQuery === 'undefined') {
this.id = completer.id + this.constructor.name;
this.completer = completer;
this.option = option;
this.lastCurosr = null;
if (this.option.debounce) {
this._onKeyup = debounce(this._onKeyup, this.option.debounce);
@ -866,12 +885,20 @@ if (typeof jQuery === 'undefined') {
// ---------------
_bindEvents: function () {
editor.on('cursorActivity', $.proxy(this._onKeyup, this));
$('.CodeMirror').on('touchend.' + this.id, $.proxy(this._onKeyup, this));
$('.CodeMirror').on('mouseup.' + this.id, $.proxy(this._onKeyup, this));
$(editor.getInputField()).on('focus', $.proxy(this._onKeyup, this));
this.$el.on('keyup.' + this.id, $.proxy(this._onKeyup, this));
},
_onKeyup: function (e) {
var focus = (e.type == 'focus');
var cursor = editor.getCursor();
var samePos = (cursor == this.lastCursor);
if (this._skipSearch(e)) { return; }
this.completer.trigger(this.getTextFromHeadToCaret(), true);
this.completer.trigger(this.getTextFromHeadToCaret(), focus ? false : samePos);
this.lastCursor = cursor;
},
// Suppress searching if it returns true.

8
public/vendor/jquery.mousewheel.min.js vendored Executable file
View file

@ -0,0 +1,8 @@
/*! Copyright (c) 2013 Brandon Aaron (http://brandon.aaron.sh)
* Licensed under the MIT License (LICENSE.txt).
*
* Version: 3.1.11
*
* Requires: jQuery 1.2.2+
*/
!function(a){"function"==typeof define&&define.amd?define(["jquery"],a):"object"==typeof exports?module.exports=a:a(jQuery)}(function(a){function b(b){var g=b||window.event,h=i.call(arguments,1),j=0,l=0,m=0,n=0,o=0,p=0;if(b=a.event.fix(g),b.type="mousewheel","detail"in g&&(m=-1*g.detail),"wheelDelta"in g&&(m=g.wheelDelta),"wheelDeltaY"in g&&(m=g.wheelDeltaY),"wheelDeltaX"in g&&(l=-1*g.wheelDeltaX),"axis"in g&&g.axis===g.HORIZONTAL_AXIS&&(l=-1*m,m=0),j=0===m?l:m,"deltaY"in g&&(m=-1*g.deltaY,j=m),"deltaX"in g&&(l=g.deltaX,0===m&&(j=-1*l)),0!==m||0!==l){if(1===g.deltaMode){var q=a.data(this,"mousewheel-line-height");j*=q,m*=q,l*=q}else if(2===g.deltaMode){var r=a.data(this,"mousewheel-page-height");j*=r,m*=r,l*=r}if(n=Math.max(Math.abs(m),Math.abs(l)),(!f||f>n)&&(f=n,d(g,n)&&(f/=40)),d(g,n)&&(j/=40,l/=40,m/=40),j=Math[j>=1?"floor":"ceil"](j/f),l=Math[l>=1?"floor":"ceil"](l/f),m=Math[m>=1?"floor":"ceil"](m/f),k.settings.normalizeOffset&&this.getBoundingClientRect){var s=this.getBoundingClientRect();o=b.clientX-s.left,p=b.clientY-s.top}return b.deltaX=l,b.deltaY=m,b.deltaFactor=f,b.offsetX=o,b.offsetY=p,b.deltaMode=0,h.unshift(b,j,l,m),e&&clearTimeout(e),e=setTimeout(c,200),(a.event.dispatch||a.event.handle).apply(this,h)}}function c(){f=null}function d(a,b){return k.settings.adjustOldDeltas&&"mousewheel"===a.type&&b%120===0}var e,f,g=["wheel","mousewheel","DOMMouseScroll","MozMousePixelScroll"],h="onwheel"in document||document.documentMode>=9?["wheel"]:["mousewheel","DomMouseScroll","MozMousePixelScroll"],i=Array.prototype.slice;if(a.event.fixHooks)for(var j=g.length;j;)a.event.fixHooks[g[--j]]=a.event.mouseHooks;var k=a.event.special.mousewheel={version:"3.1.11",setup:function(){if(this.addEventListener)for(var c=h.length;c;)this.addEventListener(h[--c],b,!1);else this.onmousewheel=b;a.data(this,"mousewheel-line-height",k.getLineHeight(this)),a.data(this,"mousewheel-page-height",k.getPageHeight(this))},teardown:function(){if(this.removeEventListener)for(var c=h.length;c;)this.removeEventListener(h[--c],b,!1);else this.onmousewheel=null;a.removeData(this,"mousewheel-line-height"),a.removeData(this,"mousewheel-page-height")},getLineHeight:function(b){var c=a(b)["offsetParent"in a.fn?"offsetParent":"parent"]();return c.length||(c=a("body")),parseInt(c.css("fontSize"),10)},getPageHeight:function(b){return a(b).height()},settings:{adjustOldDeltas:!0,normalizeOffset:!0}};a.fn.extend({mousewheel:function(a){return a?this.bind("mousewheel",a):this.trigger("mousewheel")},unmousewheel:function(a){return this.unbind("mousewheel",a)}})});

121
public/vendor/md-toc.js vendored Executable file
View file

@ -0,0 +1,121 @@
/**
* 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.tocTop = parseInt(options.top) || 0;
this.elChilds = this.el.children;
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);
}
}
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 = this._elTitleElement.innerHTML.replace(/<(?:.|\n)*?>/gm, '');
var id = this._elTitleElement.getAttribute('id');
if (!id) {
this._elTitleElement.setAttribute('id', 'tip' + i);
id = '#tip' + i;
} else {
id = '#' + id;
}
this.tocContent += '<li><a href="' + id + '">' + this._elTitleElementText + '</a>';
if (j != this._elTitleElementsLen) {
this._elNextTitleElementName = this.elTitleElements[j].tagName;
if (this._elTitleElementName != this._elNextTitleElementName) {
var checkColse = false,
y = 1;
for (var t = this._tempLists.length - 1; t >= 0; t--) {
if (this._tempLists[t].tagName == this._elNextTitleElementName) {
checkColse = true;
break;
}
y++;
}
if (checkColse) {
this.tocContent += new Array(y + 1).join('</li></ul>');
this._tempLists.length = this._tempLists.length - y;
} else {
this._tempLists.push(this._elTitleElement);
this.tocContent += '<ul class="nav">';
}
} else {
this.tocContent += '</li>';
}
} else {
if (this._tempLists.length) {
this.tocContent += new Array(this._tempLists.length + 1).join('</li></ul>');
} else {
this.tocContent += '</li>';
}
}
}
this.tocContent = '<ul class="nav">' + this.tocContent + '</ul>';
};
Toc.prototype._showToc = function () {
this.toc = document.createElement('div');
this.toc.innerHTML = this.tocContent;
this.toc.setAttribute('class', this.tocClass);
if (!this.options.targetId) {
this.el.appendChild(this.toc);
} else {
document.getElementById(this.options.targetId).appendChild(this.toc);
}
var self = this;
if (this.tocTop > -1) {
window.onscroll = function () {
var t = document.documentElement.scrollTop || document.body.scrollTop;
if (t < self.tocTop) {
self.toc.setAttribute('style', 'position:absolute;top:' + self.tocTop + 'px;');
} else {
self.toc.setAttribute('style', 'position:fixed;top:10px;');
}
}
}
};
window.Toc = Toc;
})(window);

1
public/vendor/md5.min.js vendored Executable file
View file

@ -0,0 +1 @@
!function(a){"use strict";function b(a,b){var c=(65535&a)+(65535&b),d=(a>>16)+(b>>16)+(c>>16);return d<<16|65535&c}function c(a,b){return a<<b|a>>>32-b}function d(a,d,e,f,g,h){return b(c(b(b(d,a),b(f,h)),g),e)}function e(a,b,c,e,f,g,h){return d(b&c|~b&e,a,b,f,g,h)}function f(a,b,c,e,f,g,h){return d(b&e|c&~e,a,b,f,g,h)}function g(a,b,c,e,f,g,h){return d(b^c^e,a,b,f,g,h)}function h(a,b,c,e,f,g,h){return d(c^(b|~e),a,b,f,g,h)}function i(a,c){a[c>>5]|=128<<c%32,a[(c+64>>>9<<4)+14]=c;var d,i,j,k,l,m=1732584193,n=-271733879,o=-1732584194,p=271733878;for(d=0;d<a.length;d+=16)i=m,j=n,k=o,l=p,m=e(m,n,o,p,a[d],7,-680876936),p=e(p,m,n,o,a[d+1],12,-389564586),o=e(o,p,m,n,a[d+2],17,606105819),n=e(n,o,p,m,a[d+3],22,-1044525330),m=e(m,n,o,p,a[d+4],7,-176418897),p=e(p,m,n,o,a[d+5],12,1200080426),o=e(o,p,m,n,a[d+6],17,-1473231341),n=e(n,o,p,m,a[d+7],22,-45705983),m=e(m,n,o,p,a[d+8],7,1770035416),p=e(p,m,n,o,a[d+9],12,-1958414417),o=e(o,p,m,n,a[d+10],17,-42063),n=e(n,o,p,m,a[d+11],22,-1990404162),m=e(m,n,o,p,a[d+12],7,1804603682),p=e(p,m,n,o,a[d+13],12,-40341101),o=e(o,p,m,n,a[d+14],17,-1502002290),n=e(n,o,p,m,a[d+15],22,1236535329),m=f(m,n,o,p,a[d+1],5,-165796510),p=f(p,m,n,o,a[d+6],9,-1069501632),o=f(o,p,m,n,a[d+11],14,643717713),n=f(n,o,p,m,a[d],20,-373897302),m=f(m,n,o,p,a[d+5],5,-701558691),p=f(p,m,n,o,a[d+10],9,38016083),o=f(o,p,m,n,a[d+15],14,-660478335),n=f(n,o,p,m,a[d+4],20,-405537848),m=f(m,n,o,p,a[d+9],5,568446438),p=f(p,m,n,o,a[d+14],9,-1019803690),o=f(o,p,m,n,a[d+3],14,-187363961),n=f(n,o,p,m,a[d+8],20,1163531501),m=f(m,n,o,p,a[d+13],5,-1444681467),p=f(p,m,n,o,a[d+2],9,-51403784),o=f(o,p,m,n,a[d+7],14,1735328473),n=f(n,o,p,m,a[d+12],20,-1926607734),m=g(m,n,o,p,a[d+5],4,-378558),p=g(p,m,n,o,a[d+8],11,-2022574463),o=g(o,p,m,n,a[d+11],16,1839030562),n=g(n,o,p,m,a[d+14],23,-35309556),m=g(m,n,o,p,a[d+1],4,-1530992060),p=g(p,m,n,o,a[d+4],11,1272893353),o=g(o,p,m,n,a[d+7],16,-155497632),n=g(n,o,p,m,a[d+10],23,-1094730640),m=g(m,n,o,p,a[d+13],4,681279174),p=g(p,m,n,o,a[d],11,-358537222),o=g(o,p,m,n,a[d+3],16,-722521979),n=g(n,o,p,m,a[d+6],23,76029189),m=g(m,n,o,p,a[d+9],4,-640364487),p=g(p,m,n,o,a[d+12],11,-421815835),o=g(o,p,m,n,a[d+15],16,530742520),n=g(n,o,p,m,a[d+2],23,-995338651),m=h(m,n,o,p,a[d],6,-198630844),p=h(p,m,n,o,a[d+7],10,1126891415),o=h(o,p,m,n,a[d+14],15,-1416354905),n=h(n,o,p,m,a[d+5],21,-57434055),m=h(m,n,o,p,a[d+12],6,1700485571),p=h(p,m,n,o,a[d+3],10,-1894986606),o=h(o,p,m,n,a[d+10],15,-1051523),n=h(n,o,p,m,a[d+1],21,-2054922799),m=h(m,n,o,p,a[d+8],6,1873313359),p=h(p,m,n,o,a[d+15],10,-30611744),o=h(o,p,m,n,a[d+6],15,-1560198380),n=h(n,o,p,m,a[d+13],21,1309151649),m=h(m,n,o,p,a[d+4],6,-145523070),p=h(p,m,n,o,a[d+11],10,-1120210379),o=h(o,p,m,n,a[d+2],15,718787259),n=h(n,o,p,m,a[d+9],21,-343485551),m=b(m,i),n=b(n,j),o=b(o,k),p=b(p,l);return[m,n,o,p]}function j(a){var b,c="";for(b=0;b<32*a.length;b+=8)c+=String.fromCharCode(a[b>>5]>>>b%32&255);return c}function k(a){var b,c=[];for(c[(a.length>>2)-1]=void 0,b=0;b<c.length;b+=1)c[b]=0;for(b=0;b<8*a.length;b+=8)c[b>>5]|=(255&a.charCodeAt(b/8))<<b%32;return c}function l(a){return j(i(k(a),8*a.length))}function m(a,b){var c,d,e=k(a),f=[],g=[];for(f[15]=g[15]=void 0,e.length>16&&(e=i(e,8*a.length)),c=0;16>c;c+=1)f[c]=909522486^e[c],g[c]=1549556828^e[c];return d=i(f.concat(k(b)),512+8*b.length),j(i(g.concat(d),640))}function n(a){var b,c,d="0123456789abcdef",e="";for(c=0;c<a.length;c+=1)b=a.charCodeAt(c),e+=d.charAt(b>>>4&15)+d.charAt(15&b);return e}function o(a){return unescape(encodeURIComponent(a))}function p(a){return l(o(a))}function q(a){return n(p(a))}function r(a,b){return m(o(a),o(b))}function s(a,b){return n(r(a,b))}function t(a,b,c){return b?c?r(b,a):s(b,a):c?p(a):q(a)}"function"==typeof define&&define.amd?define(function(){return t}):a.md5=t}(this);

125
public/vendor/showup/showup.css vendored Executable file
View file

@ -0,0 +1,125 @@
/*
* Showup.js jQuery Plugin
* http://github.com/jonschlinkert/showup
*
* Copyright (c) 2013 Jon Schlinkert, contributors
* Licensed under the MIT License (MIT).
*/
/**
* Docs navbar transitions effects
*/
.navbar-tall,
.navbar-show {
-webkit-transition: -webkit-transform .3s;
-moz-transition: -moz-transform .3s;
-o-transition: -o-transform .3s;
transition: transform .3s;
-webkit-transform: translate(0, 0);
-ms-transform: translate(0, 0);
transform: translate(0, 0);
}
.navbar-hide {
-webkit-transition: -webkit-transform .2s;
-moz-transition: -moz-transform .2s;
-o-transition: -o-transform .2s;
transition: transform .2s;
-webkit-transform: translate(0, -60px);
-ms-transform: translate(0, -60px);
transform: translate(0, -60px);
}
.navbar-tall,
.navbar-short,
.navbar-tall .navbar-brand,
.navbar-short .navbar-brand,
.navbar-tall .navbar-nav > li > a,
.navbar-short .navbar-nav > li > a {
-webkit-transition: all 0.2s linear;
transition: all 0.2s linear;
}
.navbar-short {
min-height: 40px;
}
.navbar-short .navbar-brand {
font-size: 16px;
padding: 13px 15px 10px;
}
.navbar-short .navbar-nav > li > a {
padding-top: 12px;
padding-bottom: 12px;
}
.navbar-tall {
min-height: 70px;
}
.navbar-tall .navbar-brand {
font-size: 24px;
padding: 25px 15px;
}
.navbar-tall .navbar-nav > li > a {
padding-top: 25px;
}
/**
* Docs Buttons
*/
/* Fixed button, bottom right */
.btn-fixed-bottom {
position: fixed;
bottom: 30px;
display: none;
z-index: 5;
width: 40px;
height: 40px;
}
/* Toggles navbar classes */
.btn-hide-show {
margin-right: 10px;
}
/* Light theme */
.btn-light {
color: #555;
background-color: rgba(0, 0, 0,.1);
}
.btn-light:hover {
color: #111;
background-color: rgba(0, 0, 0,.25);
}
/* Dark theme */
.btn-dark {
color: #fff;
background-color: rgba(0, 0, 0,.5);
}
.btn-dark:hover {
color: #fff;
background-color: rgba(0, 0, 0,.9);
}
/* Buttons displayed throughout the content */
.btn-showup {
position: relative;
color: #fff;
font-weight: normal;
background-color: #463265;
border-color: #3F2961;
}
.btn-showup:hover,
.btn-showup:focus {
color: #fff;
outline: none;
background-color: #39235A;
border-color: #39235A;
}

87
public/vendor/showup/showup.js vendored Executable file
View file

@ -0,0 +1,87 @@
/*
* Showup.js jQuery Plugin
* http://github.com/jonschlinkert/showup
*
* Copyright (c) 2013 Jon Schlinkert, contributors
* Licensed under the MIT License (MIT).
*/
(function( $ ) {
$.fn.showUp = function(ele, options) {
options = options || {};
var target = $(ele);
var down = options.down || 'navbar-hide';
var up = options.up || 'navbar-show';
var btnHideShow = options.btnHideShow || '.btn-hide-show';
var hideOffset = options.offset || 60;
var previousScroll = 0;
var isHide = false;
$(window).scroll(function () {
checkScrollTop();
});
$(window).resize(function () {
checkScrollTop();
});
$(window).mousewheel(function () {
checkScrollTop();
});
function checkScrollTop()
{
target.clearQueue();
target.stop();
var currentScroll = $(this).scrollTop();
if (currentScroll > hideOffset) {
if(Math.abs(previousScroll - currentScroll) < 50) return;
if (currentScroll > previousScroll) {
// Action on scroll down
target.removeClass(up).addClass(down);
} else if (currentScroll < previousScroll) {
// Action on scroll up
target.removeClass(down).addClass(up);
}
} else {
target.removeClass(down).addClass(up);
}
previousScroll = $(this).scrollTop();
}
// Toggle visibility of target on click
$(btnHideShow).click(function () {
if (target.hasClass(down)) {
target.removeClass(down).addClass(up);
} else {
target.removeClass(up).addClass(down);
}
});
};
})( jQuery );
// TODO: make customizable
$(document).ready(function () {
var duration = 420;
var showOffset = 220;
var btnFixed = '.btn-fixed-bottom';
var btnToTopClass = '.back-to-top';
$(window).scroll(function () {
if ($(this).scrollTop() > showOffset) {
$(btnFixed).fadeIn(duration);
} else {
$(btnFixed).fadeOut(duration);
}
});
$(btnToTopClass).click(function (event) {
event.preventDefault();
$('html, body').animate({
scrollTop: 0
}, duration);
return false;
});
});

View file

@ -3,7 +3,31 @@
<textarea id="textit"></textarea>
</div>
<div class="ui-view-area">
<div class="markdown-body container-fluid"></div>
<div class="ui-infobar container-fluid">
<small>
<span class="ui-lastchange text-uppercase"></span>
<span class="ui-permission dropdown pull-right">
<a id="permissionLabel" class="ui-permission-label text-uppercase" data-target="#" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">
</a>
<ul class="dropdown-menu" aria-labelledby="permissionLabel">
<li class="ui-permission-freely"><a><i class="fa fa-leaf fa-fw"></i> Freely - Anyone can edit</a></li>
<li class="ui-permission-editable"><a><i class="fa fa-pencil fa-fw"></i> Editable - Signed people can edit</a></li>
<li class="ui-permission-locked"><a><i class="fa fa-lock fa-fw"></i> Locked - Only owner can edit</a></li>
</ul>
</span>
</small>
</div>
<div id="doc" class="markdown-body container-fluid"></div>
<div class="ui-toc dropup" style="display:none;">
<div class="pull-right dropdown">
<a id="tocLabel" class="ui-toc-label btn btn-default" data-target="#" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false" title="Table of content">
<i class="fa fa-bars"></i>
</a>
<ul id="toc" class="ui-toc-dropdown dropdown-menu" aria-labelledby="tocLabel">
</ul>
</div>
</div>
<div id="toc-affix" class="ui-affix-toc ui-toc-dropdown" data-spy="affix" style="top:50px;display:none;"></div>
</div>
</div>
<div class="modal fade" id="clipboardModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
@ -24,4 +48,66 @@
</div>
</div>
</div>
</div>
<div class="modal fade" id="refreshModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="myModalLabel">This page need refresh</h4>
</div>
<div class="modal-body">
<h5>This page have a mismatch client version or incorrect user state.</h5>
<strong>Please refresh this page.</strong>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="refreshModalRefresh">Refresh</button>
</div>
</div>
</div>
</div>
<!-- signin modal -->
<div class="modal fade signin-modal" tabindex="-1" role="dialog" aria-labelledby="mySmallModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="mySmallModalLabel">Please sign in to edit</h4>
</div>
<div class="modal-body">
<a href="/auth/facebook" class="btn btn-lg btn-block btn-social btn-facebook">
<i class="fa fa-facebook"></i> Sign in via Facebook
</a>
<a href="/auth/twitter" class="btn btn-lg btn-block btn-social btn-twitter">
<i class="fa fa-twitter"></i> Sign in via Twitter
</a>
<a href="/auth/github" class="btn btn-lg btn-block btn-social btn-github">
<i class="fa fa-github"></i> Sign in via GitHub
</a>
<a href="/auth/dropbox" class="btn btn-lg btn-block btn-social btn-dropbox">
<i class="fa fa-dropbox"></i> Sign in via Dropbox
</a>
</div>
</div>
</div>
</div>
<!-- locked modal -->
<div class="modal fade locked-modal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title" id="myModalLabel"><i class="fa fa-lock"></i> This note is locked</h4>
</div>
<div class="modal-body" style="color:black;">
<h5>Sorry, only owner can edit this note.</h5>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-danger" data-dismiss="modal">OK</button>
</div>
</div>
</div>
</div>

View file

@ -1,5 +1,6 @@
<script src="/vendor/spin.min.js" defer></script>
<script src="/vendor/jquery-1.11.2.min.js" defer></script>
<script src="/vendor/jquery.mousewheel.min.js" defer></script>
<script src="/vendor/bootstrap/js/bootstrap.min.js" defer></script>
<script src="/vendor/greensock-js/TweenMax.min.js" defer></script>
<script src="/vendor/greensock-js/jquery.gsap.min.js" defer></script>
@ -13,6 +14,7 @@
<script src="/vendor/remarkable-regex.js" defer></script>
<script src="/vendor/gist-embed.js" defer></script>
<script src="/vendor/lz-string.min.js" defer></script>
<script src="/vendor/string.min.js" defer></script>
<script src="/vendor/highlight-js/highlight.min.js" defer></script>
<script src="/vendor/js.cookie.js" defer></script>
<script src="/vendor/moment-with-locales.js" defer></script>
@ -29,6 +31,9 @@
<script src="/vendor/idle.js" defer></script>
<script src="/vendor/visibility-1.2.1.min.js" defer></script>
<script src="/vendor/list.min.js" defer></script>
<script src="/vendor/md5.min.js" defer></script>
<script src="/vendor/md-toc.js" defer></script>
<script src="/vendor/showup/showup.js" defer></script>
<script type="text/javascript" src="https://www.dropbox.com/static/api/2/dropins.js" id="dropboxjs" data-app-key="rdoizrlnkuha23r" async defer></script>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({ messageStyle: "none", skipStartupTypeset: true ,tex2jax: {inlineMath: [['$','$'], ['\\(','\\)']], processEscapes: true }});

View file

@ -4,9 +4,7 @@
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="mobile-web-app-capable" content="yes">
<meta name="description" content="Realtime collaborative markdown notes on all platforms.">
<meta name="author" content="jackycute">
<title>HackMD - Collaborative notes</title>
<title><%- title %></title>
<link rel="icon" type="image/png" href="/favicon.png">
<link rel="apple-touch-icon" href="/apple-touch-icon.png">
<link rel="stylesheet" href="/vendor/bootstrap/css/bootstrap.min.css">
@ -19,8 +17,10 @@
<link rel="stylesheet" href="/vendor/codemirror/theme/monokai.css">
<link rel="stylesheet" href="/css/github-extract.css">
<link rel="stylesheet" href="/css/gist.css">
<link rel="stylesheet" href="/vendor/highlight-js/github.min.css">
<link rel="stylesheet" href="/vendor/highlight-js/github-gist.min.css">
<link rel="stylesheet" href="/vendor/emojify/css/emojify.min.css">
<link rel="stylesheet" href="/vendor/showup/showup.css">
<link rel="stylesheet" href="/css/bootstrap-social.css">
<link rel="stylesheet" href="/css/markdown.css">
<link rel="stylesheet" href="/css/index.css">
<link rel="stylesheet" href="/css/extra.css">

View file

@ -13,15 +13,18 @@
<ul class="dropdown-menu list" role="menu" aria-labelledby="menu">
</ul>
</div>
<a class="navbar-brand" href="./"><i class="fa fa-file-text"></i> HackMD</a>
<a class="navbar-brand" href="/"><i class="fa fa-file-text"></i> HackMD</a>
<div class="nav-mobile pull-right visible-xs">
<span class="btn btn-link btn-file ui-upload-image" title="Upload Image" style="display:none;">
<i class="fa fa-camera"></i><input type="file" accept="image/*" name="upload" multiple>
</span>
<a data-target="#" data-toggle="dropdown" class="btn btn-link">
<i class="fa fa-caret-down"></i>
</a>
<ul class="dropdown-menu" role="menu" aria-labelledby="menu">
<li role="presentation"><a role="menuitem" class="ui-new" tabindex="-1" href="./new" target="_blank"><i class="fa fa-plus fa-fw"></i> New</a>
</li>
<li role="presentation"><a role="menuitem" class="ui-pretty" tabindex="-1" href="#" target="_blank"><i class="fa fa-share-alt fa-fw"></i> Share</a>
<li role="presentation"><a role="menuitem" class="ui-share" tabindex="-1" href="#" target="_blank"><i class="fa fa-share-alt fa-fw"></i> Share</a>
</li>
<li class="divider"></li>
<li class="dropdown-header">Save</li>
@ -62,6 +65,9 @@
<a href="https://www.facebook.com/messages/866415986748945" class="btn btn-link ui-feedback" title="Feedback" target="_blank">
<i class="fa fa-bullhorn"></i>
</a>
<span class="btn btn-link btn-file ui-upload-image" title="Upload Image" style="display:none;">
<i class="fa fa-camera"></i><input type="file" accept="image/*" name="upload" multiple>
</span>
</ul>
<ul class="nav navbar-nav navbar-right">
<li id="online-user-list">
@ -79,7 +85,7 @@
</a>
</li>
<li>
<a href="#" target="_blank" class="ui-pretty">
<a href="#" target="_blank" class="ui-share">
<i class="fa fa-share-alt"></i> Share
</a>
</li>

View file

@ -4,43 +4,62 @@
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>
HackMD - Collaborative notes
</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="mobile-web-app-capable" content="yes">
<title><%- title %></title>
<link rel="icon" type="image/png" href="<%- url %>/favicon.png">
<link rel="apple-touch-icon" href="<%- url %>/apple-touch-icon.png">
<link rel="stylesheet" href='<%- url %>/vendor/bootstrap/css/bootstrap.min.css'>
<link rel="stylesheet" href='<%- url %>/vendor/font-awesome/css/font-awesome.min.css'>
<link rel="stylesheet" href='<%- url %>/css/github-extract.css'>
<link rel="stylesheet" href='<%- url %>/css/gist.css'>
<link rel="stylesheet" href='<%- url %>/vendor/highlight-js/github.min.css'>
<link rel="stylesheet" href='<%- url %>/vendor/highlight-js/github-gist.min.css'>
<link rel="stylesheet" href='<%- url %>/css/markdown.css'>
<link rel="stylesheet" href='<%- url %>/vendor/emojify/css/emojify.min.css'>
<link rel="stylesheet" href='<%- url %>/css/extra.css'>
<link rel="stylesheet" href='<%- url %>/css/site.css'>
</head>
<body>
<div class="container markdown-body" style="display:none;">
<body style="display:none;">
<div class="ui-infobar container-fluid">
<small>
<span class="ui-lastchange text-uppercase"><%- updatetime %></span>
<span class="pull-right"><%- viewcount %> views <a href="#" class="ui-edit" title="Edit this note"><i class="fa fa-pencil"></i></a></span>
</small>
</div>
<div id="doc" class="container markdown-body">
<%- body %>
</div>
<div class="ui-toc dropup" style="display:none;">
<div class="pull-right dropdown">
<a id="tocLabel" class="ui-toc-label btn btn-default" data-target="#" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false" title="Table of content">
<i class="fa fa-bars"></i>
</a>
<ul id="toc" class="ui-toc-dropdown dropdown-menu" aria-labelledby="tocLabel">
</ul>
</div>
</div>
<div id="toc-affix" class="ui-affix-toc ui-toc-dropdown" data-spy="affix" style="display:none;"></div>
</body>
</html>
<!--<script src="<%- url %>/js/ga.js" async defer></script>-->
<!--<script src="<%- url %>/js/newrelic.js" async defer></script>-->
<script src="<%- url %>/vendor/jquery-1.11.2.min.js" defer></script>
<script src="<%- url %>/vendor/bootstrap/js/bootstrap.min.js" defer></script>
<script src="<%- url %>/vendor/lz-string.min.js" defer></script>
<script src="<%- url %>/vendor/remarkable.min.js" defer></script>
<script src="<%- url %>/vendor/remarkable-regex.js" defer></script>
<script src="<%- url %>/vendor/gist-embed.js" defer></script>
<script src="<%- url %>/vendor/string.min.js" defer></script>
<script src="<%- url %>/vendor/highlight-js/highlight.min.js" defer></script>
<script src="<%- url %>/vendor/moment-with-locales.js" defer></script>
<script src="<%- url %>/vendor/emojify/js/emojify.min.js" defer></script>
<script src="<%- url %>/vendor/raphael-min.js" defer></script>
<script src="<%- url %>/vendor/lodash.min.js" defer></script>
<script src="<%- url %>/vendor/sequence-diagrams/sequence-diagram-min.js" defer></script>
<script src="<%- url %>/vendor/flowchart/flowchart-1.4.0.min.js" defer></script>
<script src="<%- url %>/vendor/md-toc.js" defer></script>
<script type="text/x-mathjax-config">
MathJax.Hub.Config({ messageStyle: "none", skipStartupTypeset: true ,tex2jax: {inlineMath: [['$','$'], ['\\(','\\)']], processEscapes: true }});
</script>

2
run.sh
View file

@ -5,4 +5,4 @@ MONGOLAB_URI='change this' \
PORT='80' \
SSLPORT='443' \
DOMAIN='change this' \
forever -a --uid hackmd start app.js
forever -a --uid hackmd -l ./logs/hackmd_log.log -o ./logs/hackmd_out.log -e ./logs/hackmd_error.log start app.js

0
tmp/.keep Normal file
View file