Merge branch 'master' into frontend-next

This commit is contained in:
Yukai Huang 2017-01-02 15:09:19 +08:00
commit 65acaea8cf
26 changed files with 307 additions and 111 deletions

View file

@ -1,13 +1,19 @@
List of HackMD contributors. List of HackMD contributors.
bananaapple
Bartlomiej Szala Bartlomiej Szala
Colin Maudry
Dmytro Kytsmen Dmytro Kytsmen
Fabien Meghazi Fabien Meghazi
Florian Rhiem
Ikumi Shimizu Ikumi Shimizu
ivanorsolic ivanorsolic
Jason Croft Jason Croft
Jannik Lorenz Jannik Lorenz
James Stephenson
Jordan Matelsky Jordan Matelsky
Kenji Doi
Lars Kajes
Lapinot Lapinot
Laura Kyle Laura Kyle
Marcelo Alencar Marcelo Alencar
@ -17,10 +23,13 @@ Max Wu
Ömer Erdinç Yağmurlu Ömer Erdinç Yağmurlu
p0v1n0m p0v1n0m
Pablo Guerrero Pablo Guerrero
paraschadha2052
Peter Dave Hello Peter Dave Hello
Qubo Qubo
Sergio Valverde Sergio Valverde
Tom Wyckhuys
Yukai Huang Yukai Huang
Zacharias Traianos Zacharias Traianos
Zankio Zankio
Xavier
葉家郡 葉家郡

View file

@ -1,6 +1,6 @@
The MIT License (MIT) The MIT License (MIT)
Copyright (c) 2016 Max Wu <jackymaxj@gmail.com> and others Copyright (c) 2017 Max Wu <jackymaxj@gmail.com> and others
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View file

@ -22,6 +22,12 @@ You can quickly setup a sample heroku hackmd application by clicking the button
[![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy) [![Deploy](https://www.herokucdn.com/deploy/button.svg)](https://heroku.com/deploy)
[migration-to-0.5.0](https://github.com/hackmdio/migration-to-0.5.0)
---
We don't use LZString to compress socket.io data and DB data after version 0.5.0.
Please run the migration tool if you're upgrading from the old version.
[migration-to-0.4.0](https://github.com/hackmdio/migration-to-0.4.0) [migration-to-0.4.0](https://github.com/hackmdio/migration-to-0.4.0)
--- ---

5
app.js
View file

@ -11,6 +11,7 @@ var compression = require('compression')
var session = require('express-session'); var session = require('express-session');
var SequelizeStore = require('connect-session-sequelize')(session.Store); var SequelizeStore = require('connect-session-sequelize')(session.Store);
var fs = require('fs'); var fs = require('fs');
var url = require('url');
var path = require('path'); var path = require('path');
var imgur = require('imgur'); var imgur = require('imgur');
var formidable = require('formidable'); var formidable = require('formidable');
@ -102,7 +103,7 @@ app.use(helmet.hsts({
})); }));
i18n.configure({ i18n.configure({
locales: ['en', 'zh', 'fr', 'de', 'ja', 'es', 'el', 'pt', 'it', 'tr', 'ru', 'nl', 'hr', 'pl', 'uk', 'hi', 'sv'], locales: ['en', 'zh', 'fr', 'de', 'ja', 'es', 'el', 'pt', 'it', 'tr', 'ru', 'nl', 'hr', 'pl', 'uk', 'hi', 'sv', 'eo'],
cookie: 'locale', cookie: 'locale',
directory: __dirname + '/locales' directory: __dirname + '/locales'
}); });
@ -487,7 +488,7 @@ app.post('/uploadimage', function (req, res) {
switch (config.imageUploadType) { switch (config.imageUploadType) {
case 'filesystem': case 'filesystem':
res.send({ res.send({
link: path.join(config.serverurl, files.image.path.match(/^public(.+$)/)[1]) link: url.resolve(config.serverurl, files.image.path.match(/^public(.+$)/)[1])
}); });
break; break;

View file

@ -35,11 +35,16 @@
"description": "sub url path, like `www.example.com/<URL_PATH>`", "description": "sub url path, like `www.example.com/<URL_PATH>`",
"required": false "required": false
}, },
"HMD_ALLOW_ORIGIN": { "HMD_PORT": {
"description": "web app port", "description": "web app port",
"required": false, "required": false,
"value": "80" "value": "80"
}, },
"HMD_ALLOW_ORIGIN": {
"description": "domain name whitelist (use comma to separate)",
"required": false,
"value": "localhost"
},
"HMD_PROTOCOL_USESSL": { "HMD_PROTOCOL_USESSL": {
"description": "set to use ssl protocol for resources path (only applied when domain is set)", "description": "set to use ssl protocol for resources path (only applied when domain is set)",
"required": false "required": false

View file

@ -1,7 +1,9 @@
{ {
"test": { "test": {
"db": {
"dialect": "sqlite", "dialect": "sqlite",
"storage": "./db.hackmd.sqlite" "storage": "./db.hackmd.sqlite"
}
}, },
"development": { "development": {
"domain": "localhost", "domain": "localhost",

View file

@ -111,8 +111,8 @@ function getserverurl() {
return url; return url;
} }
var version = '0.4.6'; var version = '0.5.0';
var minimumCompatibleVersion = '0.4.5'; var minimumCompatibleVersion = '0.5.0';
var maintenance = true; var maintenance = true;
var cwd = path.join(__dirname, '..'); var cwd = path.join(__dirname, '..');

View file

@ -4,7 +4,10 @@ module.exports = {
up: function (queryInterface, Sequelize) { up: function (queryInterface, Sequelize) {
queryInterface.addColumn('Notes', 'savedAt', Sequelize.DATE); queryInterface.addColumn('Notes', 'savedAt', Sequelize.DATE);
queryInterface.createTable('Revisions', { queryInterface.createTable('Revisions', {
id: Sequelize.UUID, id: {
type: Sequelize.UUID,
primaryKey: true
},
noteId: Sequelize.UUID, noteId: Sequelize.UUID,
patch: Sequelize.TEXT, patch: Sequelize.TEXT,
lastContent: Sequelize.TEXT, lastContent: Sequelize.TEXT,

View file

@ -20,6 +20,19 @@ if (config.dburl)
else else
sequelize = new Sequelize(dbconfig.database, dbconfig.username, dbconfig.password, dbconfig); sequelize = new Sequelize(dbconfig.database, dbconfig.username, dbconfig.password, dbconfig);
// [Postgres] Handling NULL bytes
// https://github.com/sequelize/sequelize/issues/6485
function stripNullByte(value) {
return value ? value.replace(/\u0000/g, "") : value;
}
sequelize.stripNullByte = stripNullByte;
function processData(data, _default, process) {
if (data === undefined) return data;
else return data === null ? _default : (process ? process(data) : data);
}
sequelize.processData = processData;
var db = {}; var db = {};
fs fs

View file

@ -52,13 +52,31 @@ module.exports = function (sequelize, DataTypes) {
defaultValue: 0 defaultValue: 0
}, },
title: { title: {
type: DataTypes.TEXT type: DataTypes.TEXT,
get: function () {
return sequelize.processData(this.getDataValue('title'), "");
},
set: function (value) {
this.setDataValue('title', sequelize.stripNullByte(value));
}
}, },
content: { content: {
type: DataTypes.TEXT type: DataTypes.TEXT,
get: function () {
return sequelize.processData(this.getDataValue('content'), "");
},
set: function (value) {
this.setDataValue('content', sequelize.stripNullByte(value));
}
}, },
authorship: { authorship: {
type: DataTypes.TEXT type: DataTypes.TEXT,
get: function () {
return sequelize.processData(this.getDataValue('authorship'), [], JSON.parse);
},
set: function (value) {
this.setDataValue('authorship', JSON.stringify(value));
}
}, },
lastchangeAt: { lastchangeAt: {
type: DataTypes.DATE type: DataTypes.DATE
@ -124,8 +142,6 @@ module.exports = function (sequelize, DataTypes) {
var body = fs.readFileSync(filePath, 'utf8'); var body = fs.readFileSync(filePath, 'utf8');
var contentLength = body.length; var contentLength = body.length;
var title = Note.parseNoteTitle(body); var title = Note.parseNoteTitle(body);
body = LZString.compressToBase64(body);
title = LZString.compressToBase64(title);
if (fsModifiedTime.isAfter(dbModifiedTime) && note.content !== body) { if (fsModifiedTime.isAfter(dbModifiedTime) && note.content !== body) {
note.update({ note.update({
title: title, title: title,
@ -135,14 +151,14 @@ module.exports = function (sequelize, DataTypes) {
sequelize.models.Revision.saveNoteRevision(note, function (err, revision) { sequelize.models.Revision.saveNoteRevision(note, function (err, revision) {
if (err) return _callback(err, null); if (err) return _callback(err, null);
// update authorship on after making revision of docs // update authorship on after making revision of docs
var patch = dmp.patch_fromText(LZString.decompressFromBase64(revision.patch)); var patch = dmp.patch_fromText(revision.patch);
var operations = Note.transformPatchToOperations(patch, contentLength); var operations = Note.transformPatchToOperations(patch, contentLength);
var authorship = note.authorship ? JSON.parse(LZString.decompressFromBase64(note.authorship)) : []; var authorship = note.authorship;
for (var i = 0; i < operations.length; i++) { for (var i = 0; i < operations.length; i++) {
authorship = Note.updateAuthorshipByOperation(operations[i], null, authorship); authorship = Note.updateAuthorshipByOperation(operations[i], null, authorship);
} }
note.update({ note.update({
authorship: LZString.compressToBase64(JSON.stringify(authorship)) authorship: JSON.stringify(authorship)
}).then(function (note) { }).then(function (note) {
return callback(null, note.id); return callback(null, note.id);
}).catch(function (err) { }).catch(function (err) {
@ -264,10 +280,7 @@ module.exports = function (sequelize, DataTypes) {
return markdown.substr(0, 100).replace(/(?:\r\n|\r|\n)/g, ' '); return markdown.substr(0, 100).replace(/(?:\r\n|\r|\n)/g, ' ');
}, },
decodeTitle: function (title) { decodeTitle: function (title) {
var decodedTitle = LZString.decompressFromBase64(title); return title ? title : 'Untitled';
if (decodedTitle) title = decodedTitle;
else title = 'Untitled';
return title;
}, },
generateWebTitle: function (title) { generateWebTitle: function (title) {
title = !title || title == "Untitled" ? "HackMD - Collaborative markdown notes" : title + " - HackMD"; title = !title || title == "Untitled" ? "HackMD - Collaborative markdown notes" : title + " - HackMD";
@ -496,8 +509,8 @@ module.exports = function (sequelize, DataTypes) {
if (Note.checkFileExist(filePath)) { if (Note.checkFileExist(filePath)) {
var fsCreatedTime = moment(fs.statSync(filePath).ctime); var fsCreatedTime = moment(fs.statSync(filePath).ctime);
body = fs.readFileSync(filePath, 'utf8'); body = fs.readFileSync(filePath, 'utf8');
note.title = LZString.compressToBase64(Note.parseNoteTitle(body)); note.title = Note.parseNoteTitle(body);
note.content = LZString.compressToBase64(body); note.content = body;
if (filePath !== config.defaultnotepath) { if (filePath !== config.defaultnotepath) {
note.createdAt = fsCreatedTime; note.createdAt = fsCreatedTime;
} }

View file

@ -2,7 +2,6 @@
// external modules // external modules
var Sequelize = require("sequelize"); var Sequelize = require("sequelize");
var LZString = require('lz-string');
var async = require('async'); var async = require('async');
var moment = require('moment'); var moment = require('moment');
var childProcess = require('child_process'); var childProcess = require('child_process');
@ -60,19 +59,43 @@ module.exports = function (sequelize, DataTypes) {
defaultValue: Sequelize.UUIDV4 defaultValue: Sequelize.UUIDV4
}, },
patch: { patch: {
type: DataTypes.TEXT type: DataTypes.TEXT,
get: function () {
return sequelize.processData(this.getDataValue('patch'), "");
},
set: function (value) {
this.setDataValue('patch', sequelize.stripNullByte(value));
}
}, },
lastContent: { lastContent: {
type: DataTypes.TEXT type: DataTypes.TEXT,
get: function () {
return sequelize.processData(this.getDataValue('lastContent'), "");
},
set: function (value) {
this.setDataValue('lastContent', sequelize.stripNullByte(value));
}
}, },
content: { content: {
type: DataTypes.TEXT type: DataTypes.TEXT,
get: function () {
return sequelize.processData(this.getDataValue('content'), "");
},
set: function (value) {
this.setDataValue('content', sequelize.stripNullByte(value));
}
}, },
length: { length: {
type: DataTypes.INTEGER type: DataTypes.INTEGER
}, },
authorship: { authorship: {
type: DataTypes.TEXT type: DataTypes.TEXT,
get: function () {
return sequelize.processData(this.getDataValue('authorship'), [], JSON.parse);
},
set: function (value) {
this.setDataValue('authorship', value ? JSON.stringify(value) : value);
}
} }
}, { }, {
classMethods: { classMethods: {
@ -214,7 +237,7 @@ module.exports = function (sequelize, DataTypes) {
Revision.create({ Revision.create({
noteId: note.id, noteId: note.id,
lastContent: note.content, lastContent: note.content,
length: LZString.decompressFromBase64(note.content).length, length: note.content.length,
authorship: note.authorship authorship: note.authorship
}).then(function (revision) { }).then(function (revision) {
Revision.finishSaveNoteRevision(note, revision, callback); Revision.finishSaveNoteRevision(note, revision, callback);
@ -223,8 +246,8 @@ module.exports = function (sequelize, DataTypes) {
}); });
} else { } else {
var latestRevision = revisions[0]; var latestRevision = revisions[0];
var lastContent = LZString.decompressFromBase64(latestRevision.content || latestRevision.lastContent); var lastContent = latestRevision.content || latestRevision.lastContent;
var content = LZString.decompressFromBase64(note.content); var content = note.content;
sendDmpWorker({ sendDmpWorker({
msg: 'create patch', msg: 'create patch',
lastDoc: lastContent, lastDoc: lastContent,
@ -244,9 +267,9 @@ module.exports = function (sequelize, DataTypes) {
} else { } else {
Revision.create({ Revision.create({
noteId: note.id, noteId: note.id,
patch: LZString.compressToBase64(patch), patch: patch,
content: note.content, content: note.content,
length: LZString.decompressFromBase64(note.content).length, length: note.content.length,
authorship: note.authorship authorship: note.authorship
}).then(function (revision) { }).then(function (revision) {
// clear last revision content to reduce db size // clear last revision content to reduce db size

View file

@ -7,7 +7,6 @@ var Server = require('./server');
var Selection = require('./selection'); var Selection = require('./selection');
var util = require('util'); var util = require('util');
var LZString = require('lz-string');
var logger = require('../logger'); var logger = require('../logger');
function EditorSocketIOServer(document, operations, docId, mayWrite, operationCallback) { function EditorSocketIOServer(document, operations, docId, mayWrite, operationCallback) {
@ -40,10 +39,8 @@ EditorSocketIOServer.prototype.addClient = function (socket) {
revision: this.operations.length, revision: this.operations.length,
clients: this.users clients: this.users
}; };
socket.emit('doc', LZString.compressToUTF16(JSON.stringify(docOut))); socket.emit('doc', docOut);
socket.on('operation', function (revision, operation, selection) { socket.on('operation', function (revision, operation, selection) {
operation = LZString.decompressFromUTF16(operation);
operation = JSON.parse(operation);
socket.origin = 'operation'; socket.origin = 'operation';
self.mayWrite(socket, function (mayWrite) { self.mayWrite(socket, function (mayWrite) {
if (!mayWrite) { if (!mayWrite) {
@ -62,7 +59,7 @@ EditorSocketIOServer.prototype.addClient = function (socket) {
clients: self.users, clients: self.users,
force: true force: true
}; };
socket.emit('doc', LZString.compressToUTF16(JSON.stringify(docOut))); socket.emit('doc', docOut);
}, 100); }, 100);
} }
}); });
@ -129,7 +126,6 @@ EditorSocketIOServer.prototype.onGetOperations = function (socket, base, head) {
var operations = this.operations.slice(base, head).map(function (op) { var operations = this.operations.slice(base, head).map(function (op) {
return op.wrapped.toJSON(); return op.wrapped.toJSON();
}); });
operations = LZString.compressToUTF16(JSON.stringify(operations));
socket.emit('operations', head, operations); socket.emit('operations', head, operations);
}; };

View file

@ -71,7 +71,6 @@ function emitCheck(note) {
authors: note.authors, authors: note.authors,
authorship: note.authorship authorship: note.authorship
}; };
out = LZString.compressToUTF16(JSON.stringify(out));
realtime.io.to(note.id).emit('check', out); realtime.io.to(note.id).emit('check', out);
} }
@ -153,12 +152,10 @@ function finishUpdateNote(note, _note, callback) {
if (!note || !note.server) return callback(null, null); if (!note || !note.server) return callback(null, null);
var body = note.server.document; var body = note.server.document;
var title = note.title = models.Note.parseNoteTitle(body); var title = note.title = models.Note.parseNoteTitle(body);
title = LZString.compressToBase64(title);
body = LZString.compressToBase64(body);
var values = { var values = {
title: title, title: title,
content: body, content: body,
authorship: LZString.compressToBase64(JSON.stringify(note.authorship)), authorship: note.authorship,
lastchangeuserId: note.lastchangeuser, lastchangeuserId: note.lastchangeuser,
lastchangeAt: Date.now() lastchangeAt: Date.now()
}; };
@ -301,7 +298,6 @@ function emitOnlineUsers(socket) {
var out = { var out = {
users: users users: users
}; };
out = LZString.compressToUTF16(JSON.stringify(out));
realtime.io.to(noteId).emit('online users', out); realtime.io.to(noteId).emit('online users', out);
} }
@ -330,7 +326,6 @@ function emitRefresh(socket) {
createtime: note.createtime, createtime: note.createtime,
updatetime: note.updatetime updatetime: note.updatetime
}; };
out = LZString.compressToUTF16(JSON.stringify(out));
socket.emit('refresh', out); socket.emit('refresh', out);
} }
@ -462,7 +457,7 @@ function startConnection(socket) {
var lastchangeuser = note.lastchangeuserId; var lastchangeuser = note.lastchangeuserId;
var lastchangeuserprofile = note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null; var lastchangeuserprofile = note.lastchangeuser ? models.User.getProfile(note.lastchangeuser) : null;
var body = LZString.decompressFromBase64(note.content); var body = note.content;
var createtime = note.createdAt; var createtime = note.createdAt;
var updatetime = note.lastchangeAt; var updatetime = note.lastchangeAt;
var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit, operationCallback); var server = new ot.EditorSocketIOServer(body, [], noteId, ifMayEdit, operationCallback);
@ -482,7 +477,7 @@ function startConnection(socket) {
notes[noteId] = { notes[noteId] = {
id: noteId, id: noteId,
alias: note.alias, alias: note.alias,
title: LZString.decompressFromBase64(note.title), title: note.title,
owner: owner, owner: owner,
ownerprofile: ownerprofile, ownerprofile: ownerprofile,
permission: note.permission, permission: note.permission,
@ -494,7 +489,7 @@ function startConnection(socket) {
updatetime: moment(updatetime).valueOf(), updatetime: moment(updatetime).valueOf(),
server: server, server: server,
authors: authors, authors: authors,
authorship: note.authorship ? JSON.parse(LZString.decompressFromBase64(note.authorship)) : [] authorship: note.authorship
}; };
return finishConnection(socket, notes[noteId], users[socket.id]); return finishConnection(socket, notes[noteId], users[socket.id]);
@ -863,7 +858,6 @@ function connection(socket) {
var out = { var out = {
users: users users: users
}; };
out = LZString.compressToUTF16(JSON.stringify(out));
socket.emit('online users', out); socket.emit('online users', out);
}); });

View file

@ -75,7 +75,7 @@ function showIndex(req, res, next) {
} }
function responseHackMD(res, note) { function responseHackMD(res, note) {
var body = LZString.decompressFromBase64(note.content); var body = note.content;
var meta = null; var meta = null;
try { try {
meta = models.Note.parseMeta(metaMarked(body).meta); meta = models.Note.parseMeta(metaMarked(body).meta);
@ -191,7 +191,7 @@ function showPublishNote(req, res, next) {
if (!note) { if (!note) {
return response.errorNotFound(res); return response.errorNotFound(res);
} }
var body = LZString.decompressFromBase64(note.content); var body = note.content;
var meta = null; var meta = null;
var markdown = null; var markdown = null;
try { try {
@ -209,7 +209,7 @@ function showPublishNote(req, res, next) {
var origin = config.serverurl; var origin = config.serverurl;
var data = { var data = {
title: title, title: title,
description: meta.description || markdown ? models.Note.generateDescription(markdown) : null, description: meta.description || (markdown ? models.Note.generateDescription(markdown) : null),
viewcount: note.viewcount, viewcount: note.viewcount,
createtime: createtime, createtime: createtime,
updatetime: updatetime, updatetime: updatetime,
@ -248,7 +248,7 @@ function actionSlide(req, res, note) {
} }
function actionDownload(req, res, note) { function actionDownload(req, res, note) {
var body = LZString.decompressFromBase64(note.content); var body = note.content;
var title = models.Note.decodeTitle(note.title); var title = models.Note.decodeTitle(note.title);
var filename = title; var filename = title;
filename = encodeURIComponent(filename); filename = encodeURIComponent(filename);
@ -265,7 +265,7 @@ function actionDownload(req, res, note) {
} }
function actionInfo(req, res, note) { function actionInfo(req, res, note) {
var body = LZString.decompressFromBase64(note.content); var body = note.content;
var meta = null; var meta = null;
var markdown = null; var markdown = null;
try { try {
@ -281,7 +281,7 @@ function actionInfo(req, res, note) {
var title = models.Note.decodeTitle(note.title); var title = models.Note.decodeTitle(note.title);
var data = { var data = {
title: meta.title || title, title: meta.title || title,
description: meta.description || markdown ? models.Note.generateDescription(markdown) : null, description: meta.description || (markdown ? models.Note.generateDescription(markdown) : null),
viewcount: note.viewcount, viewcount: note.viewcount,
createtime: createtime, createtime: createtime,
updatetime: updatetime updatetime: updatetime
@ -297,7 +297,7 @@ function actionInfo(req, res, note) {
} }
function actionPDF(req, res, note) { function actionPDF(req, res, note) {
var body = LZString.decompressFromBase64(note.content); var body = note.content;
try { try {
body = metaMarked(body).markdown; body = metaMarked(body).markdown;
} catch(err) { } catch(err) {
@ -479,7 +479,7 @@ function githubActionGist(req, res, note) {
if (!error && httpResponse.statusCode == 200) { if (!error && httpResponse.statusCode == 200) {
var access_token = body.access_token; var access_token = body.access_token;
if (access_token) { if (access_token) {
var content = LZString.decompressFromBase64(note.content); var content = note.content;
var title = models.Note.decodeTitle(note.title); var title = models.Note.decodeTitle(note.title);
var filename = title.replace('/', ' ') + '.md'; var filename = title.replace('/', ' ') + '.md';
var gist = { var gist = {
@ -579,7 +579,7 @@ function showPublishSlide(req, res, next) {
if (!note) { if (!note) {
return response.errorNotFound(res); return response.errorNotFound(res);
} }
var body = LZString.decompressFromBase64(note.content); var body = note.content;
var meta = null; var meta = null;
var markdown = null; var markdown = null;
try { try {
@ -597,7 +597,7 @@ function showPublishSlide(req, res, next) {
var origin = config.serverurl; var origin = config.serverurl;
var data = { var data = {
title: title, title: title,
description: meta.description || markdown ? models.Note.generateDescription(markdown) : null, description: meta.description || (markdown ? models.Note.generateDescription(markdown) : null),
viewcount: note.viewcount, viewcount: note.viewcount,
createtime: createtime, createtime: createtime,
updatetime: updatetime, updatetime: updatetime,

View file

@ -1,5 +1,4 @@
// external modules // external modules
var LZString = require('lz-string');
var DiffMatchPatch = require('diff-match-patch'); var DiffMatchPatch = require('diff-match-patch');
var dmp = new DiffMatchPatch(); var dmp = new DiffMatchPatch();
@ -58,7 +57,6 @@ process.on('message', function(data) {
function createPatch(lastDoc, currDoc) { function createPatch(lastDoc, currDoc) {
var ms_start = (new Date()).getTime(); var ms_start = (new Date()).getTime();
var diff = dmp.diff_main(lastDoc, currDoc); var diff = dmp.diff_main(lastDoc, currDoc);
dmp.diff_cleanupSemantic(diff);
var patch = dmp.patch_make(lastDoc, diff); var patch = dmp.patch_make(lastDoc, diff);
patch = dmp.patch_toText(patch); patch = dmp.patch_toText(patch);
var ms_end = (new Date()).getTime(); var ms_end = (new Date()).getTime();
@ -80,10 +78,10 @@ function getRevision(revisions, count) {
for (var i = 0; i < count; i++) { for (var i = 0; i < count; i++) {
var revision = revisions[i]; var revision = revisions[i];
if (i == 0) { if (i == 0) {
startContent = LZString.decompressFromBase64(revision.content || revision.lastContent); startContent = revision.content || revision.lastContent;
} }
if (i != count - 1) { if (i != count - 1) {
var patch = dmp.patch_fromText(LZString.decompressFromBase64(revision.patch)); var patch = dmp.patch_fromText(revision.patch);
applyPatches = applyPatches.concat(patch); applyPatches = applyPatches.concat(patch);
} }
lastPatch = revision.patch; lastPatch = revision.patch;
@ -105,11 +103,11 @@ function getRevision(revisions, count) {
for (var i = l; i >= count - 1; i--) { for (var i = l; i >= count - 1; i--) {
var revision = revisions[i]; var revision = revisions[i];
if (i == l) { if (i == l) {
startContent = LZString.decompressFromBase64(revision.lastContent); startContent = revision.lastContent;
authorship = revision.authorship; authorship = revision.authorship;
} }
if (revision.patch) { if (revision.patch) {
var patch = dmp.patch_fromText(LZString.decompressFromBase64(revision.patch)); var patch = dmp.patch_fromText(revision.patch);
applyPatches = applyPatches.concat(patch); applyPatches = applyPatches.concat(patch);
} }
lastPatch = revision.patch; lastPatch = revision.patch;
@ -123,8 +121,8 @@ function getRevision(revisions, count) {
} }
var data = { var data = {
content: finalContent, content: finalContent,
patch: dmp.patch_fromText(LZString.decompressFromBase64(lastPatch)), patch: dmp.patch_fromText(lastPatch),
authorship: authorship ? JSON.parse(LZString.decompressFromBase64(authorship)) : null authorship: authorship
}; };
var ms_end = (new Date()).getTime(); var ms_end = (new Date()).getTime();
if (config.debug) { if (config.debug) {

104
locales/eo.json Normal file
View file

@ -0,0 +1,104 @@
{
"Collaborative markdown notes": "Kunlaborataj marksubenaj notoj",
"Realtime collaborative markdown notes on all platforms.": "Tujkunlaborataj marksubenaj notoj ĉe ĉiuj sistemoj.",
"Best way to write and share your knowledge in markdown.": "La plej bona maniero skribi kaj havigi vian scion marksubene.",
"Intro": "Enkonduko",
"History": "Historio",
"New guest note": "Novan gastan noton",
"Collaborate with URL": "Kunlaboru per URL",
"Support charts and MathJax": "Ebleco por skemoj kaj MathJax",
"Support slide mode": "Ebleco por bildvica modo",
"Sign In": "Ensalutu",
"Below is the history from browser": "Malsupre estas la historio de la retumilo",
"Welcome!": "Bonvenon!",
"New note": "Novan Noton",
"or": "aŭ",
"Sign Out": "Elsalutu",
"Explore all features": "Esploru ĉiujn eblecojn",
"Select tags...": "Elektu etikedojn..",
"Search keyword...": "Serĉu ĉefvorton...",
"Sort by title": "Ordigu laŭ titolo",
"Title": "Titolo",
"Sort by time": "Ordigu laŭ tempo",
"Time": "Tempo",
"Export history": "Elportu historion",
"Import history": "Alportu historion",
"Clear history": "Malplenigu historion",
"Refresh history": "Refreŝigu historion",
"No history": "Neniu historio",
"Import from browser": "Alportu de retumilo",
"Releases": "Eldonoj",
"Are you sure?": "Ĉu vi certas?",
"Cancel": "Nuligu",
"Yes, do it!": "Jes, faru ĝin!",
"Choose method": "Elektu metodon",
"Sign in via %s": "Ensalutu per %s",
"New": "Nova",
"Publish": "Dissendu",
"Extra": "Plia",
"Revision": "Versio",
"Slide Mode": "Bildvica modo",
"Export": "Elportu",
"Import": "Alportu",
"Clipboard": "Poŝo",
"Download": "Elŝuti",
"Raw HTML": "Kruda HTML",
"Edit": "Redaktu",
"View": "Vidu",
"Both": "Ambaŭ",
"Help": "Helpo",
"Upload Image": "Alŝutu bildon",
"Menu": "Menuo",
"This page need refresh": "Ĉi tiu paĝo bezonas refreŝiĝi",
"You have an incompatible client version.": "Vi havas malkongruan klientversion.",
"Refresh to update.": "Refreŝigu por ĝisdatigi",
"New version available!": "Nova versio disponeblas!",
"See releases notes here": "Vidu elsendajn notojn ĉi tie",
"Refresh to enjoy new features.": "Refreŝigu por ĝui novajn eblecojn.",
"Your user state has changed.": "Via uzantstato ŝanĝiĝis.",
"Refresh to load new user state.": "Refreŝigu por ŝargi novan uzantstaton.",
"Refresh": "Refreŝigu",
"Contacts": "Kontaktuloj",
"Report an issue": "Raportu problemon",
"Send us email": "Sendu al ni retpoŝton",
"Documents": "Dosieroj",
"Features": "Eblecoj",
"YAML Metadata": "YAML metadateno",
"Slide Example": "Bildvica ekzemplo",
"Cheatsheet": "Gvidfolio",
"Example": "Ekzemplo",
"Syntax": "Sintakso",
"Header": "Paĝokapo",
"Unordered List": "Neordita Listo",
"Ordered List": "Ordita Listo",
"Todo List": "Farenda Listo",
"Blockquote": "Deŝovita cito",
"Bold font": "Dika tiparo",
"Italics font": "Kursiva tiparo",
"Strikethrough": "Trastrekita",
"Inserted text": "Enmetita teksto",
"Marked text": "Markita teksto",
"Link": "Ligilo",
"Image": "Bildo",
"Code": "Kodo",
"Externals": "Eksteraĵoj",
"This is a alert area.": "Ĉi tiu estas avertzono.",
"Revert": "Malfaru ŝanĝojn",
"Import from clipboard": "Alportu de la poŝo",
"Paste your markdown or webpage here...": "Algluu vian marksubenon aŭ retpaĝaron ĉi tie...",
"Clear": "Malplenigu",
"This note is locked": "Ĉi tiu noto estas ŝlosita",
"Sorry, only owner can edit this note.": "Bedaŭrinde, nur la proprulo povas redakti ĉi tiun noton.",
"OK": "Bone",
"Reach the limit": "Atingi la limigon",
"Sorry, you've reached the max length this note can be.": "Pardonon, ĉi tiu noto jam atingis maksimuman longecon.",
"Please reduce the content or divide it to more notes, thank you!": "Bonvolu malpligrandigi la enhavaĵon, aŭ dividi ĝin en pliajn notojn!",
"Import from Gist": "Alportu el Gist",
"Paste your gist url here...": "Algluu vian gist-an URL-n ĉi tie...",
"Import from Snippet": "Alportu el tekstero",
"Select From Available Projects": "Elektu el disponeblaj projektoj",
"Select From Available Snippets": "Elektu el disponeblaj teksteroj",
"OR": "AŬ",
"Export to Snippet": "Elportu al Snippet",
"Select Visibility Level": "Elektu videblecan nivelon"
}

View file

@ -9,12 +9,12 @@
"Support charts and MathJax": "Supporte les graphiques et MathJax", "Support charts and MathJax": "Supporte les graphiques et MathJax",
"Support slide mode": "Supporte le mode présentation", "Support slide mode": "Supporte le mode présentation",
"Sign In": "Se connecter", "Sign In": "Se connecter",
"Below is the history from browser": "En dessous ce situe l'historique du navigateur", "Below is the history from browser": "Ci-dessous, l'historique du navigateur",
"Welcome!": "Bienvenue !", "Welcome!": "Bienvenue !",
"New note": "Nouvelle note", "New note": "Nouvelle note",
"or": "ou", "or": "ou",
"Sign Out": "Se déconnecter", "Sign Out": "Se déconnecter",
"Explore all features": "Explorer toutes les fonctionnalitées", "Explore all features": "Explorer toutes les fonctionnalités",
"Select tags...": "Selectionner les tags...", "Select tags...": "Selectionner les tags...",
"Search keyword...": "Chercher un mot-clef...", "Search keyword...": "Chercher un mot-clef...",
"Sort by title": "Trier par titre", "Sort by title": "Trier par titre",
@ -28,7 +28,7 @@
"No history": "Pas d'historique", "No history": "Pas d'historique",
"Import from browser": "Importer depuis le navigateur", "Import from browser": "Importer depuis le navigateur",
"Releases": "Versions", "Releases": "Versions",
"Are you sure?": "Etes-vous sûr?", "Are you sure?": "Ëtes-vous sûr ?",
"Cancel": "Annuler", "Cancel": "Annuler",
"Yes, do it!": "Oui, je suis sûr !", "Yes, do it!": "Oui, je suis sûr !",
"Choose method": "Choisir la méthode", "Choose method": "Choisir la méthode",
@ -37,13 +37,13 @@
"Publish": "Publier", "Publish": "Publier",
"Extra": "Extra", "Extra": "Extra",
"Revision": "Historique", "Revision": "Historique",
"Slide Mode": "Mode Présentation", "Slide Mode": "Mode présentation",
"Export": "Exporter", "Export": "Exporter",
"Import": "Importer", "Import": "Importer",
"Clipboard": "Presse-papier", "Clipboard": "Presse-papier",
"Download": "Télécharger", "Download": "Télécharger",
"Raw HTML": "HTML Brut", "Raw HTML": "HTML brut",
"Edit": "Editer", "Edit": "Éditer",
"View": "Voir", "View": "Voir",
"Both": "Les deux", "Both": "Les deux",
"Help": "Aide", "Help": "Aide",
@ -54,23 +54,23 @@
"Refresh to update.": "Recharger pour mettre à jour.", "Refresh to update.": "Recharger pour mettre à jour.",
"New version available!": "Nouvelle version disponible !", "New version available!": "Nouvelle version disponible !",
"See releases notes here": "Voir les commentaires de version ici", "See releases notes here": "Voir les commentaires de version ici",
"Refresh to enjoy new features.": "Recharger pour bénéficier des nouvelles fonctionnalitées.", "Refresh to enjoy new features.": "Recharger pour bénéficier des nouvelles fonctionnalités.",
"Your user state has changed.": "Votre status utilisateur a changé.", "Your user state has changed.": "Votre statut utilisateur a changé.",
"Refresh to load new user state.": "Recharger pour avoir le nouveau statut utilisateur.", "Refresh to load new user state.": "Recharger pour avoir le nouveau statut utilisateur.",
"Refresh": "Recharger", "Refresh": "Recharger",
"Contacts": "Contacts", "Contacts": "Contacts",
"Report an issue": "Signaler un problème", "Report an issue": "Signaler un problème",
"Send us email": "Envoyez-nous un mail", "Send us email": "Envoyez-nous un mail",
"Documents": "Documents", "Documents": "Documents",
"Features": "Fonctionnalitées", "Features": "Fonctionnalités",
"YAML Metadata": "Métadonnées YAML", "YAML Metadata": "Métadonnées YAML",
"Slide Example": "Exemple de présentation", "Slide Example": "Exemple de présentation",
"Cheatsheet": "Pense-bête", "Cheatsheet": "Pense-bête",
"Example": "Exemple", "Example": "Exemple",
"Syntax": "Syntaxe", "Syntax": "Syntaxe",
"Header": "Entête", "Header": "En-tête",
"Unordered List": "Liste non-ordonnée", "Unordered List": "Liste à puce",
"Ordered List": "List ordonnée", "Ordered List": "List numérotée",
"Todo List": "Liste de tâches", "Todo List": "Liste de tâches",
"Blockquote": "Citation", "Blockquote": "Citation",
"Bold font": "Gras", "Bold font": "Gras",
@ -94,7 +94,7 @@
"Sorry, you've reached the max length this note can be.": "Désolé, vous avez atteint la longueur maximale que cette note peut avoir.", "Sorry, you've reached the max length this note can be.": "Désolé, vous avez atteint la longueur maximale que cette note peut avoir.",
"Please reduce the content or divide it to more notes, thank you!": "Merci de réduire le contenu ou de le diviser en plusieurs notes!", "Please reduce the content or divide it to more notes, thank you!": "Merci de réduire le contenu ou de le diviser en plusieurs notes!",
"Import from Gist": "Importer depuis Gist", "Import from Gist": "Importer depuis Gist",
"Paste your gist url here...": "Coller votre URL Gist ici...", "Paste your gist url here...": "Coller l'URL de votre Gist ici...",
"Import from Snippet": "Importer depuis Snippet", "Import from Snippet": "Importer depuis Snippet",
"Select From Available Projects": "Sélectionner depuis les projets disponibles", "Select From Available Projects": "Sélectionner depuis les projets disponibles",
"Select From Available Snippets": "Sélectionner depuis les Snippets disponibles", "Select From Available Snippets": "Sélectionner depuis les Snippets disponibles",

View file

@ -1,6 +1,6 @@
{ {
"name": "hackmd", "name": "hackmd",
"version": "0.4.6", "version": "0.5.0",
"description": "Realtime collaborative markdown notes on all platforms.", "description": "Realtime collaborative markdown notes on all platforms.",
"main": "app.js", "main": "app.js",
"license": "MIT", "license": "MIT",
@ -13,7 +13,7 @@
"dependencies": { "dependencies": {
"Idle.Js": "github:shawnmclean/Idle.js", "Idle.Js": "github:shawnmclean/Idle.js",
"async": "^2.1.4", "async": "^2.1.4",
"aws-sdk": "^2.7.15", "aws-sdk": "^2.7.20",
"blueimp-md5": "^2.6.0", "blueimp-md5": "^2.6.0",
"body-parser": "^1.15.2", "body-parser": "^1.15.2",
"bootstrap": "^3.3.7", "bootstrap": "^3.3.7",
@ -38,7 +38,7 @@
"formidable": "^1.0.17", "formidable": "^1.0.17",
"gist-embed": "~2.6.0", "gist-embed": "~2.6.0",
"handlebars": "^4.0.6", "handlebars": "^4.0.6",
"helmet": "^3.1.0", "helmet": "^3.3.0",
"highlight.js": "~9.9.0", "highlight.js": "~9.9.0",
"i18n": "^0.8.3", "i18n": "^0.8.3",
"imgur": "git+https://github.com/hackmdio/node-imgur.git", "imgur": "git+https://github.com/hackmdio/node-imgur.git",
@ -54,7 +54,7 @@
"keymaster": "^1.6.2", "keymaster": "^1.6.2",
"list.js": "^1.3.0", "list.js": "^1.3.0",
"list.pagination.js": "^0.1.1", "list.pagination.js": "^0.1.1",
"lodash": "^4.17.2", "lodash": "^4.17.4",
"lz-string": "1.4.4", "lz-string": "1.4.4",
"markdown-it": "^8.2.2", "markdown-it": "^8.2.2",
"markdown-it-abbr": "^1.0.4", "markdown-it-abbr": "^1.0.4",
@ -98,7 +98,7 @@
"reveal.js": "^3.3.0", "reveal.js": "^3.3.0",
"scrypt": "^6.0.3", "scrypt": "^6.0.3",
"select2": "^3.5.2-browserify", "select2": "^3.5.2-browserify",
"sequelize": "^3.27.0", "sequelize": "^3.28.0",
"sequelize-cli": "^2.5.1", "sequelize-cli": "^2.5.1",
"shortid": "2.2.6", "shortid": "2.2.6",
"socket.io": "~1.7.2", "socket.io": "~1.7.2",
@ -118,7 +118,7 @@
"vue": "^2.1.6", "vue": "^2.1.6",
"vue-loader": "^10.0.2", "vue-loader": "^10.0.2",
"winston": "^2.3.0", "winston": "^2.3.0",
"xss": "^0.3.2" "xss": "^0.3.3"
}, },
"engines": { "engines": {
"node": ">=4.x" "node": ">=4.x"
@ -154,7 +154,7 @@
"expose-loader": "^0.7.1", "expose-loader": "^0.7.1",
"extract-text-webpack-plugin": "^1.0.1", "extract-text-webpack-plugin": "^1.0.1",
"file-loader": "^0.9.0", "file-loader": "^0.9.0",
"html-webpack-plugin": "^2.24.1", "html-webpack-plugin": "^2.25.0",
"imports-loader": "^0.7.0", "imports-loader": "^0.7.0",
"json-loader": "^0.5.4", "json-loader": "^0.5.4",
"less": "^2.7.1", "less": "^2.7.1",

View file

@ -34,7 +34,7 @@ This will automatically upload the image to **[imgur](http://imgur.com)**, nothi
## Share Notes: ## Share Notes:
If you want to share an **editable** note, just copy the URL. If you want to share an **editable** note, just copy the URL.
If you want to share a **read-only** note, simply press share button <i class="fa fa-share-alt"></i> and copy the URL. If you want to share a **read-only** note, simply press publish button <i class="fa fa-share-square-o"></i> and copy the URL.
## Save a Note: ## Save a Note:
Currently, you can save to **Dropbox** <i class="fa fa-dropbox"></i> or save an `.md` file <i class="fa fa-file-text"></i> locally. Currently, you can save to **Dropbox** <i class="fa fa-dropbox"></i> or save an `.md` file <i class="fa fa-file-text"></i> locally.

View file

@ -1,6 +1,35 @@
Release Notes Release Notes
=== ===
<i class="fa fa-tag"></i> 0.5.0 `Ristretto` <i class="fa fa-clock-o"></i> 2017-01-02 02:35
---
### Enhancements
* Update year to 2017 (Happy New Year!)
* Update to improve editor performance by debounce checkEditorScrollbar event
* Refactor data processing to model definition
* Update to remove null byte on editor changes
* Update to remove null byte before saving to DB
* Update to support Esperanto locale
* Little improvements (typos, uppercase + accents, better case) for French locale
* Update features.md publish button name and icon
### Fixes
* Fix authorship might losing update event because of throttling
* Fix migration script of revision lacks of definition of primary key
* Fix to not use diff_cleanupSemantic
* Fix URL concatenation when uploading images to local filesystem
* Fix js-url not import correctly
* Fixed typo: anonmyous
* Fix codemirror spell checker not considering abbreviation which contain apostrophe in word
* Fix possible user is undefined in realtime events
* Fix wrong package name reference in webpack config for bootstrap-validator
* Fix email option in config not parse correctly
* Fix mathjax not able to render issue
### Removes
- Remove LZString compression for data storage
- Remove LZString compression for some socket.io event data
<i class="fa fa-tag"></i> 0.4.6 `Melya` <i class="fa fa-clock-o"></i> 2016-12-19 17:20 <i class="fa fa-tag"></i> 0.4.6 `Melya` <i class="fa fa-clock-o"></i> 2016-12-19 17:20
--- ---
### Features ### Features

View file

@ -12,7 +12,7 @@ window.serverurl = window.location.protocol + '//' + (domain ? domain : window.l
var noteid = urlpath ? window.location.pathname.slice(urlpath.length + 1, window.location.pathname.length).split('/')[1] : window.location.pathname.split('/')[1]; var noteid = urlpath ? window.location.pathname.slice(urlpath.length + 1, window.location.pathname.length).split('/')[1] : window.location.pathname.split('/')[1];
var noteurl = serverurl + '/' + noteid; var noteurl = serverurl + '/' + noteid;
var version = '0.4.6'; var version = '0.5.0';
var checkAuth = false; var checkAuth = false;
var profile = null; var profile = null;

View file

@ -11,7 +11,6 @@ require('highlight.js/styles/github-gist.css');
var toMarkdown = require('to-markdown'); var toMarkdown = require('to-markdown');
var saveAs = require('file-saver').saveAs; var saveAs = require('file-saver').saveAs;
var url = require('js-url');
var randomColor = require('randomcolor'); var randomColor = require('randomcolor');
var _ = require("lodash"); var _ = require("lodash");
@ -1225,7 +1224,11 @@ function checkSyncToggle() {
} }
} }
function checkEditorScrollbar() { var checkEditorScrollbar = _.debounce(function () {
editor.operation(checkEditorScrollbarInner);
}, 50);
function checkEditorScrollbarInner() {
// workaround simple scroll bar knob // workaround simple scroll bar knob
// will get wrong position when editor height changed // will get wrong position when editor height changed
var scrollInfo = editor.getScrollInfo(); var scrollInfo = editor.getScrollInfo();
@ -2445,7 +2448,7 @@ function updateInfo(data) {
updateAuthorship(); updateAuthorship();
} }
} }
var updateAuthorship = _.throttle(function () { var updateAuthorship = _.debounce(function () {
editor.operation(updateAuthorshipInner); editor.operation(updateAuthorshipInner);
}, 50); }, 50);
function initMark() { function initMark() {
@ -2647,8 +2650,6 @@ editor.on('update', function () {
}); });
}); });
socket.on('check', function (data) { socket.on('check', function (data) {
data = LZString.decompressFromUTF16(data);
data = JSON.parse(data);
//console.log(data); //console.log(data);
updateInfo(data); updateInfo(data);
}); });
@ -2658,8 +2659,6 @@ socket.on('permission', function (data) {
var docmaxlength = null; var docmaxlength = null;
var permission = null; var permission = null;
socket.on('refresh', function (data) { socket.on('refresh', function (data) {
data = LZString.decompressFromUTF16(data);
data = JSON.parse(data);
//console.log(data); //console.log(data);
docmaxlength = data.docmaxlength; docmaxlength = data.docmaxlength;
editor.setOption("maxLength", docmaxlength); editor.setOption("maxLength", docmaxlength);
@ -2706,8 +2705,6 @@ var CodeMirrorAdapter = ot.CodeMirrorAdapter;
var cmClient = null; var cmClient = null;
socket.on('doc', function (obj) { socket.on('doc', function (obj) {
obj = LZString.decompressFromUTF16(obj);
obj = JSON.parse(obj);
var body = obj.str; var body = obj.str;
var bodyMismatch = editor.getValue() !== body; var bodyMismatch = editor.getValue() !== body;
var havePendingOperation = cmClient && Object.keys(cmClient.state).length > 0; var havePendingOperation = cmClient && Object.keys(cmClient.state).length > 0;
@ -2768,8 +2765,6 @@ socket.on('operation', function () {
}); });
socket.on('online users', function (data) { socket.on('online users', function (data) {
data = LZString.decompressFromUTF16(data);
data = JSON.parse(data);
if (debug) if (debug)
console.debug(data); console.debug(data);
onlineUsers = data.users; onlineUsers = data.users;
@ -3217,6 +3212,12 @@ function buildCursor(user) {
} }
//editor actions //editor actions
function removeNullByte(cm, change) {
var str = change.text.join("\n");
if (/\u0000/g.test(str) && change.update) {
change.update(change.from, change.to, str.replace(/\u0000/g, "").split("\n"));
}
}
function enforceMaxLength(cm, change) { function enforceMaxLength(cm, change) {
var maxLength = cm.getOption("maxLength"); var maxLength = cm.getOption("maxLength");
if (maxLength && change.update) { if (maxLength && change.update) {
@ -3238,6 +3239,7 @@ var ignoreEmitEvents = ['setValue', 'ignoreHistory'];
editor.on('beforeChange', function (cm, change) { editor.on('beforeChange', function (cm, change) {
if (debug) if (debug)
console.debug(change); console.debug(change);
removeNullByte(cm, change);
if (enforceMaxLength(cm, change)) { if (enforceMaxLength(cm, change)) {
$('.limit-modal').modal('show'); $('.limit-modal').modal('show');
} }

File diff suppressed because one or more lines are too long

3
public/vendor/ot/socketio-adapter.js vendored Normal file → Executable file
View file

@ -24,8 +24,6 @@ ot.SocketIOAdapter = (function () {
self.trigger('selection', clientId, selection); self.trigger('selection', clientId, selection);
}); });
socket.on('operations', function (head, operations) { socket.on('operations', function (head, operations) {
operations = LZString.decompressFromUTF16(operations);
operations = JSON.parse(operations);
self.trigger('operations', head, operations); self.trigger('operations', head, operations);
}); });
socket.on('selection', function (clientId, selection) { socket.on('selection', function (clientId, selection) {
@ -37,7 +35,6 @@ ot.SocketIOAdapter = (function () {
} }
SocketIOAdapter.prototype.sendOperation = function (revision, operation, selection) { SocketIOAdapter.prototype.sendOperation = function (revision, operation, selection) {
operation = LZString.compressToUTF16(JSON.stringify(operation));
this.socket.emit('operation', revision, operation, selection); this.socket.emit('operation', revision, operation, selection);
}; };

View file

@ -150,7 +150,7 @@
<iframe src="//ghbtns.com/github-btn.html?user=hackmdio&repo=hackmd&type=star&count=true" frameborder="0" scrolling="0" width="104px" height="20px"></iframe> <iframe src="//ghbtns.com/github-btn.html?user=hackmdio&repo=hackmd&type=star&count=true" frameborder="0" scrolling="0" width="104px" height="20px"></iframe>
</h6> </h6>
<p> <p>
&copy; 2016 <a href="https://www.facebook.com/hackmdio" target="_blank"><i class="fa fa-facebook-square"></i> HackMD</a> | <a href="<%- url %>/s/release-notes" target="_blank"><%= __('Releases') %></a> &copy; 2017 <a href="https://www.facebook.com/hackmdio" target="_blank"><i class="fa fa-facebook-square"></i> HackMD</a> | <a href="<%- url %>/s/release-notes" target="_blank"><%= __('Releases') %></a>
</p> </p>
<select class="ui-locale"> <select class="ui-locale">
<option value="en">English</option> <option value="en">English</option>
@ -170,6 +170,7 @@
<option value="uk">Українська</option> <option value="uk">Українська</option>
<option value="hi">हिन्दी</option> <option value="hi">हिन्दी</option>
<option value="sv">svenska</option> <option value="sv">svenska</option>
<option value="eo">Esperanto</option>
</select> </select>
</div> </div>
</div> </div>

View file

@ -172,12 +172,12 @@ module.exports = {
"script!listPagnation", "script!listPagnation",
"expose?select2!select2", "expose?select2!select2",
"expose?moment!moment", "expose?moment!moment",
"js-url", "script!js-url",
path.join(__dirname, 'public/js/cover.js') path.join(__dirname, 'public/js/cover.js')
], ],
index: [ index: [
"script!jquery-ui-resizable", "script!jquery-ui-resizable",
"js-url", "script!js-url",
"expose?filterXSS!xss", "expose?filterXSS!xss",
"script!Idle.Js", "script!Idle.Js",
"expose?LZString!lz-string", "expose?LZString!lz-string",
@ -227,7 +227,7 @@ module.exports = {
"expose?jsyaml!js-yaml", "expose?jsyaml!js-yaml",
"script!mermaid", "script!mermaid",
"expose?moment!moment", "expose?moment!moment",
"js-url", "script!js-url",
"script!handlebars", "script!handlebars",
"expose?hljs!highlight.js", "expose?hljs!highlight.js",
"expose?emojify!emojify.js", "expose?emojify!emojify.js",