diff options
Diffstat (limited to 'lib/models')
-rw-r--r-- | lib/models/note.js | 238 |
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: { |