Marked as 0.2.9

This commit is contained in:
Wu Cheng-Han 2015-06-01 18:04:25 +08:00
parent 4e64583a0b
commit f7f8c901f4
33 changed files with 2972 additions and 242 deletions

View file

@ -1,11 +1,26 @@
HackMD 0.2.8
HackMD 0.2.9
===
This is a realtime collaborative markdown notes on all platforms.
But still in early stage, feel free to fork or contribute to it.
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!
Thanks for your using! :smile:
Dependency
---
- PostgreSQL 9.3.6 or 9.4.1
- MongoDB 3.0.2
Import db schema
---
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.
Config
---
There are some config you need to change in below files
```
./run.sh
@ -13,13 +28,25 @@ There are some config you need to change in below files
./public/js/common.js
```
You can use SSL to encrypt your site by passing certificate path in the `config.js` and set `usessl=true`.
And there is a script called `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.
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.
To install `forever`, just type `npm install forever -g`
The notes are store in PostgreSQL, and I provided the schema in the `hackmd_schema.sql`.
The users and sessions are store in mongoDB, which don't need schema, so just connect it directly.
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`
Stop a server
---
To stop the server, simply type `forever stop hackmd`
Backup db
---
To backup the db, type `bash backup.sh`
Backup files will be at `./backups/`
**License under MIT.**

40
app.js
View file

@ -5,6 +5,7 @@ var toobusy = require('toobusy-js');
var ejs = require('ejs');
var passport = require('passport');
var methodOverride = require('method-override');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');
var mongoose = require('mongoose');
var compression = require('compression')
@ -14,9 +15,12 @@ var fs = require('fs');
var shortid = require('shortid');
var imgur = require('imgur');
var formidable = require('formidable');
var morgan = require('morgan');
var passportSocketIo = require("passport.socketio");
//core
var config = require("./config.js");
var logger = require("./lib/logger.js");
var User = require("./lib/user.js");
var Temp = require("./lib/temp.js");
var auth = require("./lib/auth.js");
@ -45,7 +49,12 @@ 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
}));
// connect to the mongodb
mongoose.connect(process.env.MONGOLAB_URI || config.mongodbstring);
@ -65,6 +74,15 @@ var urlencodedParser = bodyParser.urlencoded({
extended: false
});
//session store
var sessionStore = new MongoStore({
mongooseConnection: mongoose.connection,
touchAfter: config.sessiontouch
},
function (err) {
console.log(err);
});
//compression
app.use(compression());
@ -79,13 +97,7 @@ app.use(session({
expires: new Date(Date.now() + config.sessionlife),
},
maxAge: new Date(Date.now() + config.sessionlife),
store: new MongoStore({
mongooseConnection: mongoose.connection,
touchAfter: config.sessiontouch
},
function (err) {
console.log(err);
})
store: sessionStore
}));
//middleware which blocks requests when we're too busy
@ -293,6 +305,7 @@ app.get('/me', function (req, res) {
var profile = JSON.parse(user.profile);
res.send({
status: 'ok',
id: req.session.passport.user,
name: profile.displayName || profile.username
});
}
@ -317,7 +330,9 @@ app.post('/uploadimage', function (req, res) {
.then(function (json) {
if (config.debug)
console.log('SERVER uploadimage success: ' + JSON.stringify(json));
res.send({link:json.data.link});
res.send({
link: json.data.link
});
})
.catch(function (err) {
console.error(err);
@ -337,6 +352,15 @@ app.get("/:noteId/:action", response.noteActions);
//socket.io secure
io.use(realtime.secure);
//socket.io auth
io.use(passportSocketIo.authorize({
cookieParser: cookieParser,
key: config.sessionname,
secret: config.sessionsecret,
store: sessionStore,
success: realtime.onAuthorizeSuccess,
fail: realtime.onAuthorizeFail
}));
//socket.io heartbeat
io.set('heartbeat interval', config.heartbeatinterval);
io.set('heartbeat timeout', config.heartbeattimeout);

7
backup.sh Normal file
View file

@ -0,0 +1,7 @@
#!/bin/bash
path=./backups
today=$(date +"%Y%m%d")
timestamp=$(date +"%Y%m%d%H%M%S")
mkdir -p $path/$today
pg_dump hackmd > $path/$today/postgresql_$timestamp
mongodump -d hackmd -o $path/$today/mongodb_$timestamp

21
lib/logger.js Normal file
View file

@ -0,0 +1,21 @@
var winston = require('winston');
winston.emitErrs = true;
var logger = new winston.Logger({
transports: [
new winston.transports.Console({
level: 'debug',
handleExceptions: true,
json: false,
colorize: true
})
],
exitOnError: false
});
module.exports = logger;
module.exports.stream = {
write: function(message, encoding){
logger.info(message);
}
};

View file

@ -7,6 +7,8 @@ var async = require('async');
var LZString = require('lz-string');
var shortId = require('shortid');
var randomcolor = require("randomcolor");
var Chance = require('chance'),
chance = new Chance();
//core
var config = require("../config.js");
@ -18,11 +20,22 @@ var User = require("./user.js");
//public
var realtime = {
onAuthorizeSuccess: onAuthorizeSuccess,
onAuthorizeFail: onAuthorizeFail,
secure: secure,
connection: connection,
getStatus: getStatus
};
function onAuthorizeSuccess(data, accept) {
accept(null, true);
}
function onAuthorizeFail(data, message, error, accept) {
if (error) throw new Error(message);
accept(null, true);
}
function secure(socket, next) {
try {
var handshakeData = socket.request;
@ -53,8 +66,10 @@ var updater = setInterval(function () {
if (note.isDirty) {
if (config.debug)
console.log("updater found dirty note: " + key);
var title = Note.getNoteTitle(LZString.decompressFromBase64(note.body));
db.saveToDB(key, title, note.body,
var body = LZString.decompressFromUTF16(note.body);
var title = Note.getNoteTitle(body);
body = LZString.compressToBase64(body);
db.saveToDB(key, title, body,
function (err, result) {});
note.isDirty = false;
}
@ -72,7 +87,7 @@ function getStatus(callback) {
var distinctaddresses = [];
Object.keys(users).forEach(function (key) {
var value = users[key];
if(value.login)
if (value.login)
regusers++;
var found = false;
for (var i = 0; i < distinctaddresses.length; i++) {
@ -83,9 +98,9 @@ function getStatus(callback) {
}
if (!found) {
distinctaddresses.push(value.address);
if(value.login)
if (value.login)
distinctregusers++;
}
}
});
User.getUserCount(function (err, regcount) {
if (err) {
@ -129,17 +144,25 @@ function emitOnlineUsers(socket) {
Object.keys(notes[notename].users).forEach(function (key) {
var user = notes[notename].users[key];
if (user)
users.push({
id: user.id,
color: user.color,
cursor: user.cursor
});
users.push(buildUserOutData(user));
});
notes[notename].socks.forEach(function (sock) {
sock.emit('online users', {
count: notes[notename].socks.length,
var out = {
users: users
});
};
out = LZString.compressToUTF16(JSON.stringify(out));
sock.emit('online users', out);
});
}
function emitUserStatus(socket) {
var notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
notes[notename].socks.forEach(function (sock) {
if (sock != socket) {
var out = buildUserOutData(users[socket.id]);
sock.emit('user status', out);
}
});
}
@ -198,7 +221,8 @@ function startConnection(socket) {
isConnectionBusy = false;
return console.error(err);
}
var body = data.rows[0].content;
var body = LZString.decompressFromBase64(data.rows[0].content);
body = LZString.compressToUTF16(body);
notes[notename] = {
socks: [],
body: body,
@ -232,8 +256,10 @@ function disconnect(socket) {
notes[notename].socks.splice(index, 1);
}
if (Object.keys(notes[notename].users).length <= 0) {
var title = Note.getNoteTitle(LZString.decompressFromBase64(notes[notename].body));
db.saveToDB(notename, title, notes[notename].body,
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) {
@ -265,20 +291,80 @@ function disconnect(socket) {
}
}
function buildUserOutData(user) {
var out = {
id: user.id,
login: user.login,
userid: user.userid,
color: user.color,
cursor: user.cursor,
name: user.name,
idle: user.idle,
type: user.type
};
return out;
}
function updateUserData(socket, user) {
//retrieve user data from passport
if (socket.request.user && socket.request.user.logged_in) {
var profile = JSON.parse(socket.request.user.profile);
user.name = profile.displayName || profile.username;
user.userid = socket.request.user._id;
user.login = true;
} else {
user.userid = null;
user.name = 'Guest ' + chance.last();
user.login = false;
}
}
function connection(socket) {
//split notename from socket
var notename = getNotenameFromSocket(socket);
//initialize user data
//random color
var color = randomcolor({
luminosity: 'light'
});
//make sure color not duplicated or reach max random count
if (notename && notes[notename]) {
var randomcount = 0;
var maxrandomcount = 5;
var found = false;
do {
Object.keys(notes[notename].users).forEach(function (user) {
if (user.color == color) {
found = true;
return;
}
});
if (found) {
color = randomcolor({
luminosity: 'light'
});
randomcount++;
}
} while (found && randomcount < maxrandomcount);
}
//create user data
users[socket.id] = {
id: socket.id,
address: socket.handshake.address,
'user-agent': socket.handshake.headers['user-agent'],
otk: shortId.generate(),
color: randomcolor({
luminosity: 'light'
}),
color: color,
cursor: null,
login: false
login: false,
userid: null,
name: null,
idle: false,
type: null
};
updateUserData(socket, users[socket.id]);
//start connection
connectionSocketQueue.push(socket);
startConnection(socket);
@ -293,61 +379,88 @@ function connection(socket) {
notes[notename].isDirty = true;
}
});
//received user status
socket.on('user status', function (data) {
if(data)
users[socket.id].login = data.login;
var notename = getNotenameFromSocket(socket);
if (!notename) return;
if (config.debug)
console.log('SERVER received [' + notename + '] user status from [' + socket.id + ']: ' + JSON.stringify(data));
if (data) {
var user = users[socket.id];
user.idle = data.idle;
user.type = data.type;
}
emitUserStatus(socket);
});
socket.on('online users', function () {
//reveiced when user logout or changed
socket.on('user changed', function () {
console.log('user changed');
var notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
updateUserData(socket, notes[notename].users[socket.id]);
emitOnlineUsers(socket);
});
//received sync of online users request
socket.on('online users', function () {
var notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
var users = [];
Object.keys(notes[notename].users).forEach(function (key) {
var user = notes[notename].users[key];
if (user)
users.push(buildUserOutData(user));
});
var out = {
users: users
};
out = LZString.compressToUTF16(JSON.stringify(out));
socket.emit('online users', out);
});
//check version
socket.on('version', function () {
socket.emit('version', config.version);
});
//received cursor focus
socket.on('cursor focus', function (data) {
var notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
users[socket.id].cursor = data;
var out = buildUserOutData(users[socket.id]);
notes[notename].socks.forEach(function (sock) {
if (sock != socket) {
var out = {
id: socket.id,
color: users[socket.id].color,
cursor: data
};
sock.emit('cursor focus', out);
}
});
});
//received cursor activity
socket.on('cursor activity', function (data) {
var notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
users[socket.id].cursor = data;
var out = buildUserOutData(users[socket.id]);
notes[notename].socks.forEach(function (sock) {
if (sock != socket) {
var out = {
id: socket.id,
color: users[socket.id].color,
cursor: data
};
sock.emit('cursor activity', out);
}
});
});
//received cursor blur
socket.on('cursor blur', function () {
var notename = getNotenameFromSocket(socket);
if (!notename || !notes[notename]) return;
users[socket.id].cursor = null;
var out = {
id: socket.id
};
notes[notename].socks.forEach(function (sock) {
if (sock != socket) {
var out = {
id: socket.id
};
if (sock != socket) {
sock.emit('cursor blur', out);
}
@ -365,7 +478,7 @@ function connection(socket) {
socket.on('change', function (op) {
var notename = getNotenameFromSocket(socket);
if (!notename) return;
op = LZString.decompressFromBase64(op);
op = LZString.decompressFromUTF16(op);
if (op)
op = JSON.parse(op);
if (config.debug)
@ -390,10 +503,12 @@ function connection(socket) {
if (sock != socket) {
if (config.debug)
console.log('SERVER emit sync data out [' + notename + ']: ' + sock.id + ', op:' + JSON.stringify(op));
sock.emit('change', LZString.compressToBase64(JSON.stringify(op)));
sock.emit('change', LZString.compressToUTF16(JSON.stringify(op)));
}
});
break;
default:
console.log('SERVER received uncaught [' + notename + '] data changed: ' + socket.id + ', op:' + JSON.stringify(op));
}
});
}

View file

@ -1,6 +1,6 @@
{
"name": "hackmd",
"version": "0.2.8",
"version": "0.2.9",
"description": "Realtime collaborative markdown notes on all platforms.",
"main": "app.js",
"author": "jackycute",
@ -9,6 +9,7 @@
"dependencies": {
"async": "^0.9.0",
"body-parser": "^1.12.3",
"chance": "^0.7.5",
"cheerio": "^0.19.0",
"compression": "^1.4.3",
"connect-mongo": "^0.8.1",
@ -27,17 +28,20 @@
"marked": "^0.3.3",
"method-override": "^2.3.2",
"mongoose": "^4.0.2",
"morgan": "^1.5.3",
"node-uuid": "^1.4.3",
"passport": "^0.2.1",
"passport-dropbox-oauth2": "^0.1.6",
"passport-facebook": "^2.0.0",
"passport-github": "^0.1.5",
"passport-twitter": "^1.0.3",
"passport.socketio": "^3.5.1",
"pg": "4.x",
"randomcolor": "^0.2.0",
"shortid": "2.1.3",
"socket.io": "1.3.5",
"toobusy-js": "^0.4.1"
"toobusy-js": "^0.4.1",
"winston": "^1.0.0"
},
"engines": {
"node": "0.10.x"

View file

@ -20,7 +20,6 @@ form,
font-family: 'Source Code Pro', Consolas, monaco, monospace;
line-height: 18px;
font-size: 16px;
/*height: auto;*/
min-height: 100%;
overflow-y: hidden !important;
-webkit-overflow-scrolling: touch;
@ -30,7 +29,7 @@ form,
overflow-y: auto !important;
}
.CodeMirror-code {
/*padding-bottom: 72px;*/
padding-bottom: 36px;
}
.CodeMirror-linenumber {
opacity: 0.5;
@ -73,23 +72,82 @@ form,
font-size: 14px;
}
.nav-mobile {
position: relative;
position: inherit;
margin-top: 8px;
margin-bottom: 8px;
}
.nav-mobile .dropdown-menu {
left: 40%;
right: 6px;
top: 42px;
}
.nav-status {
float: right !important;
padding: 7px 8px;
}
.ui-status {
cursor: auto !important;
min-width: 120px;
background-color: transparent !important;
}
.ui-status span {
cursor: pointer;
}
.ui-short-status {
cursor: pointer;
min-width: 40px;
}
.ui-short-status:hover {
text-decoration: none;
}
.ui-user-item {
/*na*/
}
.ui-user-name {
margin-top: 2px;
}
.ui-user-icon {
font-size: 20px;
margin-top: 2px;
margin-right: 5px;
}
.ui-user-status {
margin-top: 5px;
}
.ui-user-status-online {
color: rgb(92,184,92);
}
.ui-user-status-idle {
color: rgb(240,173,78);
}
.ui-user-status-offline {
color: rgb(119,119,119);
}
.list > li > a {
overflow: hidden;
text-overflow: ellipsis;
}
#short-online-user-list .list .name {
max-width: 65%;
overflow: hidden;
text-overflow: ellipsis;
float: left;
}
#online-user-list .list .name {
max-width: 110px;
overflow: hidden;
text-overflow: ellipsis;
float: left;
}
.navbar-right {
margin-right: 0;
}
.navbar-nav > li > a {
cursor: pointer;
}
.dropdown-menu > li > a {
cursor: pointer;
}
.other-cursors {
position:relative;
z-index:3;
@ -99,10 +157,63 @@ form,
position: absolute;
border-right: none;
}
.cursortag {
cursor: pointer;
background: black;
position: absolute;
padding: 2px 7px 2px 8px;
font-size: 12px;
max-width: 150px;
text-overflow: ellipsis;
overflow: hidden;
font-family: inherit;
border-radius: .25em;
white-space: nowrap;
}
.fixfixed .navbar-fixed-top {
position: absolute !important;
}
div[contenteditable]:empty:not(:focus):before{
content:attr(data-ph);
color: gray;
}
.dropdown-menu {
max-height: 80vh;
overflow: auto;
}
.dropdown-menu::-webkit-scrollbar {
display: none;
}
.dropdown-menu .emoji {
margin-bottom: 0 !important;
}
.dropdown-menu.other-cursor {
width: auto !important;
}
.CodeMirror-scrollbar-filler {
background: inherit;
}
.unselectable {
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-o-user-select: none;
user-select: none;
}
.cm-trailing-space-a:before,
.cm-trailing-space-b:before,
.cm-trailing-space-new-line:before {
font-weight: bold;
color: hsl(30, 100%, 50%); /* a dark orange */
position: absolute;
}
.cm-trailing-space-a:before,
.cm-trailing-space-b:before {
content: '·';
}
.cm-trailing-space-new-line:before {
content: '↵';
}

View file

@ -81,8 +81,8 @@
</div>
<hr>
<form class="form-inline">
<div class="form-group">
<input class="form-control ui-use-tags" style="width:172px;" />
<div class="form-group" style="vertical-align: bottom;">
<input class="form-control ui-use-tags" style="min-width:172px;max-width:344px;" />
</div>
<div class="form-group">
<input class="search form-control" placeholder="Search anything..." />
@ -188,6 +188,8 @@
<!-- 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>
<script src="//maxcdn.bootstrapcdn.com/bootstrap/3.3.4/js/bootstrap.min.js" defer></script>
<script src="/vendor/select2/select2.min.js" defer></script>
<script src="/vendor/js.cookie.js" defer></script>

View file

@ -3,38 +3,62 @@ var domain = 'change this';
var checkAuth = false;
var profile = null;
var lastLoginState = getLoginState();
var lastUserId = getUserId();
var loginStateChangeEvent = null;
function resetCheckAuth() {
checkAuth = false;
}
function setLoginState(bool) {
function setLoginState(bool, id) {
Cookies.set('loginstate', bool, {
expires: 14
});
if (loginStateChangeEvent && bool != lastLoginState)
loginStateChangeEvent();
if (id) {
Cookies.set('userid', id, {
expires: 14
});
} else {
Cookies.remove('userid');
}
lastLoginState = bool;
lastUserId = id;
checkLoginStateChanged();
}
function checkLoginStateChanged() {
if (getLoginState() != lastLoginState || getUserId() != lastUserId) {
if(loginStateChangeEvent)
loginStateChangeEvent();
return true;
} else {
return false;
}
}
function getLoginState() {
return Cookies.get('loginstate') === "true";
}
function getUserId() {
return Cookies.get('userid');
}
function clearLoginState() {
Cookies.remove('loginstate');
}
function checkIfAuth(yesCallback, noCallback) {
var cookieLoginState = getLoginState();
if (checkLoginStateChanged())
checkAuth = false;
if (!checkAuth || typeof cookieLoginState == 'undefined') {
$.get('/me')
.done(function (data) {
if (data && data.status == 'ok') {
profile = data;
yesCallback(profile);
setLoginState(true);
setLoginState(true, data.id);
} else {
noCallback();
setLoginState(false);
@ -43,8 +67,10 @@ function checkIfAuth(yesCallback, noCallback) {
.fail(function () {
noCallback();
setLoginState(false);
})
.always(function () {
checkAuth = true;
});
checkAuth = true;
} else if (cookieLoginState) {
yesCallback(profile);
} else {

View file

@ -229,6 +229,44 @@ var source = $("#template").html();
var template = Handlebars.compile(source);
var context = {
release: [
{
version: "0.2.9",
tag: "wildfire",
date: moment("201505301400", 'YYYYMMDDhhmm').fromNow(),
detail: [
{
title: "Features",
item: [
"+ Support text auto complete",
"+ Support cursor tag and random last name",
"+ Support online user list",
"+ Support show user info in blockquote"
]
},
{
title: "Enhancements",
item: [
"* Added more code highlighting support",
"* Added more continue list support",
"* Adjust menu and history filter UI for better UX",
"* Adjust sync scoll animte to gain performance",
"* Change compression method of dynamic data",
"* Optimized render script"
]
},
{
title: "Fixes",
item: [
"* Access history fallback might get wrong",
"* Sync scroll not accurate",
"* Sync scroll reach bottom range too much",
"* Detect login state change not accurate",
"* Detect editor focus not accurate",
"* Server not handle some editor events"
]
}
]
},
{
version: "0.2.8",
tag: "flame",

View file

@ -65,6 +65,7 @@ function finishView(view) {
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) {
@ -101,6 +102,18 @@ function finishView(view) {
//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>');
@ -121,6 +134,20 @@ function postProcess(code) {
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;
}
@ -195,7 +222,7 @@ function highlightRender(code, lang) {
if (/\=$/.test(lang)) {
var lines = result.value.split('\n');
var linenumbers = [];
for (var i = 0; i < lines.length; i++) {
for (var i = 0; i < lines.length - 1; i++) {
linenumbers[i] = "<div class='linenumber'>" + (i + 1) + "</div>";
}
var linegutter = "<div class='gutter'>" + linenumbers.join('\n') + "</div>";

View file

@ -47,7 +47,7 @@ function saveHistoryToStorage(notehistory) {
if (store.enabled)
store.set('notehistory', JSON.stringify(notehistory));
else
saveHistoryToCookie(notehistory);
saveHistoryToStorage(notehistory);
}
function saveHistoryToCookie(notehistory) {
@ -146,11 +146,14 @@ function writeHistoryToServer(view) {
} catch (err) {
var notehistory = [];
}
if(!notehistory)
notehistory = [];
var newnotehistory = generateHistory(view, notehistory);
saveHistoryToServer(newnotehistory);
})
.fail(function () {
writeHistoryToCookie(view);
writeHistoryToStorage(view);
});
}
@ -160,7 +163,9 @@ function writeHistoryToCookie(view) {
} catch (err) {
var notehistory = [];
}
if(!notehistory)
notehistory = [];
var newnotehistory = generateHistory(view, notehistory);
saveHistoryToCookie(newnotehistory);
}
@ -174,6 +179,9 @@ function writeHistoryToStorage(view) {
var notehistory = data;
} else
var notehistory = [];
if(!notehistory)
notehistory = [];
var newnotehistory = generateHistory(view, notehistory);
saveHistoryToStorage(newnotehistory);
} else {
@ -241,7 +249,7 @@ function getServerHistory(callback) {
}
})
.fail(function () {
getCookieHistory(callback);
getStorageHistory(callback);
});
}
@ -282,7 +290,7 @@ function parseServerToHistory(list, callback) {
}
})
.fail(function () {
parseCookieToHistory(list, callback);
parseStorageToHistory(list, callback);
});
}

File diff suppressed because it is too large Load diff

View file

@ -144,10 +144,14 @@ md.renderer.rules.code = function (tokens, idx /*, options, env */ ) {
return '<code>' + Remarkable.utils.escapeHtml(tokens[idx].content) + '</code>';
};
//var editorScrollThrottle = 100;
var buildMapThrottle = 100;
var viewScrolling = false;
var viewScrollingDelay = 200;
var viewScrollingTimer = null;
//editor.on('scroll', _.throttle(syncScrollToView, editorScrollThrottle));
editor.on('scroll', syncScrollToView);
ui.area.view.on('scroll', function () {
viewScrolling = true;
@ -168,10 +172,12 @@ function clearMap() {
lineHeightMap = null;
}
var buildMap = _.throttle(buildMapInner, buildMapThrottle);
// Build offsets for each line (lines can be wrapped)
// That's a bit dirty to process each line everytime, but ok for demo.
// Optimizations are required only for big texts.
function buildMap() {
function buildMapInner(syncBack) {
var i, offset, nonEmptyList, pos, a, b, _lineHeightMap, linesCount,
acc, sourceLikeDiv, textarea = ui.area.codemirror,
wrap = $('.CodeMirror-wrap pre'),
@ -182,8 +188,6 @@ function buildMap() {
visibility: 'hidden',
height: 'auto',
width: wrap.width(),
padding: wrap.css('padding'),
margin: wrap.css('margin'),
'font-size': textarea.css('font-size'),
'font-family': textarea.css('font-family'),
'line-height': textarea.css('line-height'),
@ -198,21 +202,23 @@ function buildMap() {
_lineHeightMap = [];
acc = 0;
editor.getValue().split('\n').forEach(function (str) {
var lines = editor.getValue().split('\n');
for (i = 0; i < lines.length; i++) {
var str = lines[i];
var h, lh;
_lineHeightMap.push(acc);
if (str.length === 0) {
acc++;
return;
continue;
}
sourceLikeDiv.text(str);
h = parseFloat(sourceLikeDiv.css('height'));
lh = parseFloat(sourceLikeDiv.css('line-height'));
acc += Math.round(h / lh);
});
}
sourceLikeDiv.remove();
_lineHeightMap.push(acc);
linesCount = acc;
@ -224,9 +230,10 @@ function buildMap() {
nonEmptyList.push(0);
_scrollMap[0] = 0;
ui.area.markdown.find('.part').each(function (n, el) {
var $el = $(el),
t = $el.data('startline') - 1;
var parts = ui.area.markdown.find('.part').toArray();
for (i = 0; i < parts.length; i++) {
var $el = $(parts[i]),
t = $el.attr('data-startline') - 1;
if (t === '') {
return;
}
@ -235,7 +242,7 @@ function buildMap() {
nonEmptyList.push(t);
}
_scrollMap[t] = Math.round($el.offset().top + offset);
});
}
nonEmptyList.push(linesCount);
_scrollMap[linesCount] = ui.area.view[0].scrollHeight;
@ -256,6 +263,9 @@ function buildMap() {
scrollMap = _scrollMap;
lineHeightMap = _lineHeightMap;
if(loaded && syncBack)
syncScrollToView();
}
function getPartByEditorLineNo(lineNo) {
@ -290,20 +300,20 @@ function getEditorLineNoByTop(top) {
return null;
}
function syncScrollToView(_lineNo) {
function syncScrollToView(event, _lineNo) {
if (currentMode != modeType.both) return;
var lineNo, posTo;
var scrollInfo = editor.getScrollInfo();
if (!scrollMap || !lineHeightMap) {
buildMap();
buildMap(true);
return;
}
if (typeof _lineNo != "number") {
if (!_lineNo) {
var topDiffPercent, posToNextDiff;
var textHeight = editor.defaultTextHeight();
lineNo = Math.floor(scrollInfo.top / textHeight);
var lineCount = editor.lineCount();
var lastLineHeight = editor.getLineHandle(lineCount - 1).height;
//if reach last line, then scroll to end
if (scrollInfo.top + scrollInfo.clientHeight >= scrollInfo.height - lastLineHeight) {
//if reach bottom, then scroll to end
if (scrollInfo.top + scrollInfo.clientHeight >= scrollInfo.height - defaultTextHeight) {
posTo = ui.area.view[0].scrollHeight - ui.area.view.height();
} else {
topDiffPercent = (scrollInfo.top % textHeight) / textHeight;
@ -316,12 +326,18 @@ function syncScrollToView(_lineNo) {
posTo = scrollMap[lineHeightMap[_lineNo]];
}
var posDiff = Math.abs(ui.area.view.scrollTop() - posTo);
var duration = posDiff / 50;
ui.area.view.stop(true, true).animate({
scrollTop: posTo
}, duration >= 100 ? duration : 100, "linear");
/*
if (posDiff > scrollInfo.clientHeight / 5) {
var duration = posDiff / 50;
ui.area.view.stop(true).animate({
ui.area.view.stop(true, true).animate({
scrollTop: posTo
}, duration >= 50 ? duration : 100, "linear");
}, duration >= 100 ? duration : 100, "linear");
} else {
ui.area.view.stop(true).scrollTop(posTo);
ui.area.view.stop(true, true).scrollTop(posTo);
}
*/
}

View file

@ -63,7 +63,7 @@
}
for (var i = ranges.length - 1; i >= 0; i--) {
var cur = ranges[i].head;
cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1));
cm.replaceRange("", Pos(cur.line, cur.ch - 1), Pos(cur.line, cur.ch + 1), "+delete");
}
}
@ -79,7 +79,7 @@
if (!around || explode.indexOf(around) % 2 != 0) return CodeMirror.Pass;
}
cm.operation(function() {
cm.replaceSelection("\n\n", null);
cm.replaceSelection("\n\n", null, "+input");
cm.execCommand("goCharLeft");
ranges = cm.listSelections();
for (var i = 0; i < ranges.length; i++) {
@ -144,12 +144,12 @@
var sels = cm.getSelections();
for (var i = 0; i < sels.length; i++)
sels[i] = left + sels[i] + right;
cm.replaceSelections(sels, "around");
cm.replaceSelections(sels, "around", "+input");
} else if (type == "both") {
cm.replaceSelection(left + right, null);
cm.replaceSelection(left + right, null, "+input");
cm.execCommand("goCharLeft");
} else if (type == "addFour") {
cm.replaceSelection(left + left + left + left, "before");
cm.replaceSelection(left + left + left + left, "before", "+input");
cm.execCommand("goCharRight");
}
});

View file

@ -11,8 +11,8 @@
})(function(CodeMirror) {
"use strict";
var listRE = /^(\s*)(>[> ]*|[*+-]\s|(\d+)\.)(\s*)/,
emptyListRE = /^(\s*)(>[> ]*|[*+-]|(\d+)\.)(\s*)$/,
var listRE = /^(\s*)(>[> ]*|[*+-]\s|(\d+)\.)(\[\s\]\s|\[x\]\s|\s*)/,
emptyListRE = /^(\s*)(>[> ]*|[*+-]\s|(\d+)\.)(\[\s\]\s*|\[x\]\s|\s*)$/,
unorderedListRE = /[*+-]\s/;
CodeMirror.commands.newlineAndIndentContinueMarkdownList = function(cm) {

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,8 @@
uglifyjs --compress --mangle --output codemirror.min.js \
lib/codemirror.js \
addon/mode/overlay.js \
addon/mode/simple.js \
addon/mode/multiplex.js \
addon/selection/active-line.js \
addon/search/searchcursor.js \
addon/search/search.js \
@ -14,6 +16,7 @@ addon/edit/matchtags.js \
addon/edit/closetag.js \
addon/edit/continuelist.js \
addon/comment/comment.js \
addon/comment/continuecomment.js \
addon/wrap/hardwrap.js \
addon/fold/foldcode.js \
addon/fold/brace-fold.js \
@ -26,7 +29,22 @@ mode/gfm/gfm.js \
mode/javascript/javascript.js \
mode/css/css.js \
mode/htmlmixed/htmlmixed.js \
mode/htmlembedded/htmlembedded.js \
mode/clike/clike.js \
mode/clojure/clojure.js \
mode/ruby/ruby.js \
mode/python/python.js \
mode/shell/shell.js \
mode/php/php.js \
mode/sql/sql.js \
mode/coffeescript/coffeescript.js \
mode/yaml/yaml.js \
mode/jade/jade.js \
mode/lua/lua.js \
mode/cmake/cmake.js \
mode/nginx/nginx.js \
mode/perl/perl.js \
mode/sass/sass.js \
mode/r/r.js \
mode/dockerfile/dockerfile.js \
keymap/sublime.js

View file

@ -388,7 +388,7 @@
viewWidth: d.wrapper.clientWidth,
barLeft: cm.options.fixedGutter ? gutterW : 0,
docHeight: docH,
scrollHeight: docH + scrollGap(cm) + d.barHeight,
scrollHeight: docH + scrollGap(cm) + d.barHeight + textHeight(cm.display),
nativeBarWidth: d.nativeBarWidth,
gutterWidth: gutterW
};
@ -2287,9 +2287,13 @@
}
$('.other-cursor').each(function(key, value) {
var coord = cm.charCoords({line:$(value).attr('data-line'), ch:$(value).attr('data-ch')}, 'windows');
$(value)[0].style.left = coord.left + 'px';
$(value)[0].style.top = coord.top + 'px';
var line = parseInt($(value).attr('data-line'));
var ch = parseInt($(value).attr('data-ch'));
var offsetLeft = parseFloat($(value).attr('data-offset-left'));
var offsetTop = parseFloat($(value).attr('data-offset-top'));
var coord = cm.charCoords({line: line, ch: ch}, 'windows');
$(value)[0].style.left = coord.left + offsetLeft + 'px';
$(value)[0].style.top = coord.top + offsetTop + 'px';
});
}

17
public/vendor/greensock-js/TweenMax.min.js vendored Executable file

File diff suppressed because one or more lines are too long

14
public/vendor/greensock-js/jquery.gsap.min.js vendored Executable file
View file

@ -0,0 +1,14 @@
/*!
* VERSION: 0.1.11
* DATE: 2015-03-13
* UPDATES AND DOCS AT: http://greensock.com/jquery-gsap-plugin/
*
* Requires TweenLite version 1.8.0 or higher and CSSPlugin.
*
* @license Copyright (c) 2013-2015, GreenSock. All rights reserved.
* This work is subject to the terms at http://greensock.com/standard-license or for