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

View file

@ -1,6 +1,6 @@
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
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)
[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)
---

5
app.js
View file

@ -11,6 +11,7 @@ var compression = require('compression')
var session = require('express-session');
var SequelizeStore = require('connect-session-sequelize')(session.Store);
var fs = require('fs');
var url = require('url');
var path = require('path');
var imgur = require('imgur');
var formidable = require('formidable');
@ -102,7 +103,7 @@ app.use(helmet.hsts({
}));
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',
directory: __dirname + '/locales'
});
@ -487,7 +488,7 @@ app.post('/uploadimage', function (req, res) {
switch (config.imageUploadType) {
case 'filesystem':
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;

View file

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

View file

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

View file

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

View file

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

View file

@ -20,6 +20,19 @@ if (config.dburl)
else
sequelize = new Sequelize(dbconfig.database, dbconfig.username, dbconfig.password, dbconfig);
// [Postgres] Handling NULL bytes
// https://github.com/sequelize/sequelize/issues/6485
function stripNullByte(value) {
return value ? value.replace(/\u0000/g, "") : value;
}
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 = {};
fs

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,5 +1,4 @@
// external modules
var LZString = require('lz-string');
var DiffMatchPatch = require('diff-match-patch');
var dmp = new DiffMatchPatch();
@ -58,7 +57,6 @@ process.on('message', function(data) {
function createPatch(lastDoc, currDoc) {
var ms_start = (new Date()).getTime();
var diff = dmp.diff_main(lastDoc, currDoc);
dmp.diff_cleanupSemantic(diff);
var patch = dmp.patch_make(lastDoc, diff);
patch = dmp.patch_toText(patch);
var ms_end = (new Date()).getTime();
@ -80,10 +78,10 @@ function getRevision(revisions, count) {
for (var i = 0; i < count; i++) {
var revision = revisions[i];
if (i == 0) {
startContent = LZString.decompressFromBase64(revision.content || revision.lastContent);
startContent = revision.content || revision.lastContent;
}
if (i != count - 1) {
var patch = dmp.patch_fromText(LZString.decompressFromBase64(revision.patch));
var patch = dmp.patch_fromText(revision.patch);
applyPatches = applyPatches.concat(patch);
}
lastPatch = revision.patch;
@ -105,11 +103,11 @@ function getRevision(revisions, count) {
for (var i = l; i >= count - 1; i--) {
var revision = revisions[i];
if (i == l) {
startContent = LZString.decompressFromBase64(revision.lastContent);
startContent = revision.lastContent;
authorship = revision.authorship;
}
if (revision.patch) {
var patch = dmp.patch_fromText(LZString.decompressFromBase64(revision.patch));
var patch = dmp.patch_fromText(revision.patch);
applyPatches = applyPatches.concat(patch);
}
lastPatch = revision.patch;
@ -123,8 +121,8 @@ function getRevision(revisions, count) {
}
var data = {
content: finalContent,
patch: dmp.patch_fromText(LZString.decompressFromBase64(lastPatch)),
authorship: authorship ? JSON.parse(LZString.decompressFromBase64(authorship)) : null
patch: dmp.patch_fromText(lastPatch),
authorship: authorship
};
var ms_end = (new Date()).getTime();
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 slide mode": "Supporte le mode présentation",
"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 !",
"New note": "Nouvelle note",
"or": "ou",
"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...",
"Search keyword...": "Chercher un mot-clef...",
"Sort by title": "Trier par titre",
@ -28,7 +28,7 @@
"No history": "Pas d'historique",
"Import from browser": "Importer depuis le navigateur",
"Releases": "Versions",
"Are you sure?": "Etes-vous sûr?",
"Are you sure?": "Ëtes-vous sûr ?",
"Cancel": "Annuler",
"Yes, do it!": "Oui, je suis sûr !",
"Choose method": "Choisir la méthode",
@ -37,13 +37,13 @@
"Publish": "Publier",
"Extra": "Extra",
"Revision": "Historique",
"Slide Mode": "Mode Présentation",
"Slide Mode": "Mode présentation",
"Export": "Exporter",
"Import": "Importer",
"Clipboard": "Presse-papier",
"Download": "Télécharger",
"Raw HTML": "HTML Brut",
"Edit": "Editer",
"Raw HTML": "HTML brut",
"Edit": "Éditer",
"View": "Voir",
"Both": "Les deux",
"Help": "Aide",
@ -54,23 +54,23 @@
"Refresh to update.": "Recharger pour mettre à jour.",
"New version available!": "Nouvelle version disponible !",
"See releases notes here": "Voir les commentaires de version ici",
"Refresh to enjoy new features.": "Recharger pour bénéficier des nouvelles fonctionnalitées.",
"Your user state has changed.": "Votre status utilisateur a changé.",
"Refresh to enjoy new features.": "Recharger pour bénéficier des nouvelles fonctionnalités.",
"Your user state has changed.": "Votre statut utilisateur a changé.",
"Refresh to load new user state.": "Recharger pour avoir le nouveau statut utilisateur.",
"Refresh": "Recharger",
"Contacts": "Contacts",
"Report an issue": "Signaler un problème",
"Send us email": "Envoyez-nous un mail",
"Documents": "Documents",
"Features": "Fonctionnalitées",
"Features": "Fonctionnalités",
"YAML Metadata": "Métadonnées YAML",
"Slide Example": "Exemple de présentation",
"Cheatsheet": "Pense-bête",
"Example": "Exemple",
"Syntax": "Syntaxe",
"Header": "Entête",
"Unordered List": "Liste non-ordonnée",
"Ordered List": "List ordonnée",
"Header": "En-tête",
"Unordered List": "Liste à puce",
"Ordered List": "List numérotée",
"Todo List": "Liste de tâches",
"Blockquote": "Citation",
"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.",
"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",
"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",
"Select From Available Projects": "Sélectionner depuis les projets disponibles",
"Select From Available Snippets": "Sélectionner depuis les Snippets disponibles",

View file

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

View file

@ -34,7 +34,7 @@ This will automatically upload the image to **[imgur](http://imgur.com)**, nothi
## Share Notes:
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:
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
===
<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
---
### 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 noteurl = serverurl + '/' + noteid;
var version = '0.4.6';
var version = '0.5.0';
var checkAuth = false;
var profile = null;

View file

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

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);
});
socket.on('operations', function (head, operations) {
operations = LZString.decompressFromUTF16(operations);
operations = JSON.parse(operations);
self.trigger('operations', head, operations);
});
socket.on('selection', function (clientId, selection) {
@ -37,7 +35,6 @@ ot.SocketIOAdapter = (function () {
}
SocketIOAdapter.prototype.sendOperation = function (revision, operation, selection) {
operation = LZString.compressToUTF16(JSON.stringify(operation));
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>
</h6>
<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>
<select class="ui-locale">
<option value="en">English</option>
@ -170,6 +170,7 @@
<option value="uk">Українська</option>
<option value="hi">हिन्दी</option>
<option value="sv">svenska</option>
<option value="eo">Esperanto</option>
</select>
</div>
</div>

View file

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