summaryrefslogtreecommitdiff
path: root/lib/models/note.js
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--lib/models/note.js238
1 files changed, 234 insertions, 4 deletions
diff --git a/lib/models/note.js b/lib/models/note.js
index d1c073e9..7fdc5645 100644
--- a/lib/models/note.js
+++ b/lib/models/note.js
@@ -11,11 +11,17 @@ var shortId = require('shortid');
var Sequelize = require("sequelize");
var async = require('async');
var moment = require('moment');
+var DiffMatchPatch = require('diff-match-patch');
+var dmp = new DiffMatchPatch();
+var S = require('string');
// core
var config = require("../config.js");
var logger = require("../logger.js");
+//ot
+var ot = require("../ot/index.js");
+
// permission types
var permissionTypes = ["freely", "editable", "locked", "private"];
@@ -61,6 +67,7 @@ module.exports = function (sequelize, DataTypes) {
type: DataTypes.DATE
}
}, {
+ paranoid: true,
classMethods: {
associate: function (models) {
Note.belongsTo(models.User, {
@@ -115,6 +122,7 @@ module.exports = function (sequelize, DataTypes) {
var fsModifiedTime = moment(fs.statSync(filePath).mtime);
var dbModifiedTime = moment(note.lastchangeAt || note.createdAt);
var body = fs.readFileSync(filePath, 'utf8');
+ var contentLength = body.length;
var title = Note.parseNoteTitle(body);
body = LZString.compressToBase64(body);
title = LZString.compressToBase64(title);
@@ -126,7 +134,20 @@ module.exports = function (sequelize, DataTypes) {
}).then(function (note) {
sequelize.models.Revision.saveNoteRevision(note, function (err, revision) {
if (err) return _callback(err, null);
- return callback(null, note.id);
+ // update authorship on after making revision of docs
+ var patch = dmp.patch_fromText(LZString.decompressFromBase64(revision.patch));
+ var operations = Note.transformPatchToOperations(patch, contentLength);
+ var authorship = note.authorship ? JSON.parse(LZString.decompressFromBase64(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))
+ }).then(function (note) {
+ return callback(null, note.id);
+ }).catch(function (err) {
+ return _callback(err, null);
+ });
});
}).catch(function (err) {
return _callback(err, null);
@@ -198,8 +219,23 @@ module.exports = function (sequelize, DataTypes) {
return callback(null, null);
});
},
+ parseNoteInfo: function (body) {
+ var meta = null;
+ try {
+ var obj = metaMarked(body);
+ body = obj.markdown;
+ meta = obj.meta;
+ } catch (err) {
+ //na
+ }
+ if (!meta) meta = {};
+ var $ = cheerio.load(md.render(body));
+ return {
+ title: Note.extractNoteTitle(meta, $),
+ tags: Note.extractNoteTags(meta, $)
+ };
+ },
parseNoteTitle: function (body) {
- var title = "";
var meta = null;
try {
var obj = metaMarked(body);
@@ -209,13 +245,17 @@ module.exports = function (sequelize, DataTypes) {
//na
}
if (!meta) meta = {};
+ var $ = cheerio.load(md.render(body));
+ return Note.extractNoteTitle(meta, $);
+ },
+ extractNoteTitle: function (meta, $) {
+ var title = "";
if (meta.title && (typeof meta.title == "string" || typeof meta.title == "number")) {
title = meta.title;
} else {
- var $ = cheerio.load(md.render(body));
var h1s = $("h1");
if (h1s.length > 0 && h1s.first().text().split('\n').length == 1)
- title = h1s.first().text();
+ title = S(h1s.first().text()).stripTags().s;
}
if (!title) title = "Untitled";
return title;
@@ -230,6 +270,40 @@ module.exports = function (sequelize, DataTypes) {
title = !title || title == "Untitled" ? "HackMD - Collaborative markdown notes" : title + " - HackMD";
return title;
},
+ extractNoteTags: function (meta, $) {
+ var tags = [];
+ var rawtags = [];
+ if (meta.tags && (typeof meta.tags == "string" || typeof meta.tags == "number")) {
+ var metaTags = ('' + meta.tags).split(',');
+ for (var i = 0; i < metaTags.length; i++) {
+ var text = metaTags[i].trim();
+ if (text) rawtags.push(text);
+ }
+ } else {
+ var h6s = $("h6");
+ h6s.each(function (key, value) {
+ if (/^tags/gmi.test($(value).text())) {
+ var codes = $(value).find("code");
+ for (var i = 0; i < codes.length; i++) {
+ var text = $(codes[i]).html().trim();
+ if (text) rawtags.push(text);
+ }
+ }
+ });
+ }
+ for (var i = 0; i < rawtags.length; i++) {
+ var found = false;
+ for (var j = 0; j < tags.length; j++) {
+ if (tags[j] == rawtags[i]) {
+ found = true;
+ break;
+ }
+ }
+ if (!found)
+ tags.push(rawtags[i]);
+ }
+ return tags;
+ },
parseMeta: function (meta) {
var _meta = {};
if (meta) {
@@ -247,6 +321,162 @@ module.exports = function (sequelize, DataTypes) {
_meta.slideOptions = meta.slideOptions;
}
return _meta;
+ },
+ updateAuthorshipByOperation: function (operation, userId, authorships) {
+ var index = 0;
+ var timestamp = Date.now();
+ for (var i = 0; i < operation.length; i++) {
+ var op = operation[i];
+ if (ot.TextOperation.isRetain(op)) {
+ index += op;
+ } else if (ot.TextOperation.isInsert(op)) {
+ var opStart = index;
+ var opEnd = index + op.length;
+ var inserted = false;
+ // authorship format: [userId, startPos, endPos, createdAt, updatedAt]
+ if (authorships.length <= 0) authorships.push([userId, opStart, opEnd, timestamp, timestamp]);
+ else {
+ for (var j = 0; j < authorships.length; j++) {
+ var authorship = authorships[j];
+ if (!inserted) {
+ var nextAuthorship = authorships[j + 1] || -1;
+ if (nextAuthorship != -1 && nextAuthorship[1] >= opEnd || j >= authorships.length - 1) {
+ if (authorship[1] < opStart && authorship[2] > opStart) {
+ // divide
+ var postLength = authorship[2] - opStart;
+ authorship[2] = opStart;
+ authorship[4] = timestamp;
+ authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]);
+ authorships.splice(j + 2, 0, [authorship[0], opEnd, opEnd + postLength, authorship[3], timestamp]);
+ j += 2;
+ inserted = true;
+ } else if (authorship[1] >= opStart) {
+ authorships.splice(j, 0, [userId, opStart, opEnd, timestamp, timestamp]);
+ j += 1;
+ inserted = true;
+ } else if (authorship[2] <= opStart) {
+ authorships.splice(j + 1, 0, [userId, opStart, opEnd, timestamp, timestamp]);
+ j += 1;
+ inserted = true;
+ }
+ }
+ }
+ if (authorship[1] >= opStart) {
+ authorship[1] += op.length;
+ authorship[2] += op.length;
+ }
+ }
+ }
+ index += op.length;
+ } else if (ot.TextOperation.isDelete(op)) {
+ var opStart = index;
+ var opEnd = index - op;
+ if (operation.length == 1) {
+ authorships = [];
+ } else if (authorships.length > 0) {
+ for (var j = 0; j < authorships.length; j++) {
+ var authorship = authorships[j];
+ if (authorship[1] >= opStart && authorship[1] <= opEnd && authorship[2] >= opStart && authorship[2] <= opEnd) {
+ authorships.splice(j, 1);
+ j -= 1;
+ } else if (authorship[1] < opStart && authorship[1] < opEnd && authorship[2] > opStart && authorship[2] > opEnd) {
+ authorship[2] += op;
+ authorship[4] = timestamp;
+ } else if (authorship[2] >= opStart && authorship[2] <= opEnd) {
+ authorship[2] = opStart;
+ authorship[4] = timestamp;
+ } else if (authorship[1] >= opStart && authorship[1] <= opEnd) {
+ authorship[1] = opEnd;
+ authorship[4] = timestamp;
+ }
+ if (authorship[1] >= opEnd) {
+ authorship[1] += op;
+ authorship[2] += op;
+ }
+ }
+ }
+ index += op;
+ }
+ }
+ // merge
+ for (var j = 0; j < authorships.length; j++) {
+ var authorship = authorships[j];
+ for (var k = j + 1; k < authorships.length; k++) {
+ var nextAuthorship = authorships[k];
+ if (nextAuthorship && authorship[0] === nextAuthorship[0] && authorship[2] === nextAuthorship[1]) {
+ var minTimestamp = Math.min(authorship[3], nextAuthorship[3]);
+ var maxTimestamp = Math.max(authorship[3], nextAuthorship[3]);
+ authorships.splice(j, 1, [authorship[0], authorship[1], nextAuthorship[2], minTimestamp, maxTimestamp]);
+ authorships.splice(k, 1);
+ j -= 1;
+ break;
+ }
+ }
+ }
+ // clear
+ for (var j = 0; j < authorships.length; j++) {
+ var authorship = authorships[j];
+ if (!authorship[0]) {
+ authorships.splice(j, 1);
+ j -= 1;
+ }
+ }
+ return authorships;
+ },
+ transformPatchToOperations: function (patch, contentLength) {
+ var operations = [];
+ if (patch.length > 0) {
+ // calculate original content length
+ for (var j = patch.length - 1; j >= 0; j--) {
+ var p = patch[j];
+ for (var i = 0; i < p.diffs.length; i++) {
+ var diff = p.diffs[i];
+ switch(diff[0]) {
+ case 1: // insert
+ contentLength -= diff[1].length;
+ break;
+ case -1: // delete
+ contentLength += diff[1].length;
+ break;
+ }
+ }
+ }
+ // generate operations
+ var bias = 0;
+ var lengthBias = 0;
+ for (var j = 0; j < patch.length; j++) {
+ var operation = [];
+ var p = patch[j];
+ var currIndex = p.start1;
+ var currLength = contentLength - bias;
+ for (var i = 0; i < p.diffs.length; i++) {
+ var diff = p.diffs[i];
+ switch(diff[0]) {
+ case 0: // retain
+ if (i == 0) // first
+ operation.push(currIndex + diff[1].length);
+ else if (i != p.diffs.length - 1) // mid
+ operation.push(diff[1].length);
+ else // last
+ operation.push(currLength + lengthBias - currIndex);
+ currIndex += diff[1].length;
+ break;
+ case 1: // insert
+ operation.push(diff[1]);
+ lengthBias += diff[1].length;
+ currIndex += diff[1].length;
+ break;
+ case -1: // delete
+ operation.push(-diff[1].length);
+ bias += diff[1].length;
+ currIndex += diff[1].length;
+ break;
+ }
+ }
+ operations.push(operation);
+ }
+ }
+ return operations;
}
},
hooks: {