overleaf/services/document-updater/app/js/HistoryConversions.js

184 lines
4.7 KiB
JavaScript
Raw Normal View History

// @ts-check
const _ = require('lodash')
const { isDelete } = require('./Utils')
/**
* @typedef {import('./types').Comment} Comment
* @typedef {import('./types').HistoryComment} HistoryComment
* @typedef {import('./types').HistoryRanges} HistoryRanges
* @typedef {import('./types').HistoryTrackedChange} HistoryTrackedChange
* @typedef {import('./types').Ranges} Ranges
* @typedef {import('./types').TrackedChange} TrackedChange
*/
/**
* Convert editor ranges to history ranges
*
* @param {Ranges} ranges
* @return {HistoryRanges}
*/
function toHistoryRanges(ranges) {
const changes = ranges.changes ?? []
const comments = (ranges.comments ?? []).slice()
// Changes are assumed to be sorted, but not comments
comments.sort((a, b) => a.op.p - b.op.p)
/**
* This will allow us to go through comments at a different pace as we loop
* through tracked changes
*/
const commentsIterator = new CommentsIterator(comments)
/**
* Current offset between editor pos and history pos
*/
let offset = 0
/**
* History comments that might overlap with the tracked change considered
*
* @type {HistoryComment[]}
*/
let pendingComments = []
/**
* The final history comments generated
*
* @type {HistoryComment[]}
*/
const historyComments = []
/**
* The final history tracked changes generated
*
* @type {HistoryTrackedChange[]}
*/
const historyChanges = []
for (const change of changes) {
historyChanges.push(toHistoryChange(change, offset))
// After this point, we're only interested in tracked deletes
if (!isDelete(change.op)) {
continue
}
// Fill pendingComments with new comments that start before this tracked
// delete and might overlap
for (const comment of commentsIterator.nextComments(change.op.p)) {
pendingComments.push(toHistoryComment(comment, offset))
}
// Save comments that are fully before this tracked delete
const newPendingComments = []
for (const historyComment of pendingComments) {
const commentEnd = historyComment.op.p + historyComment.op.c.length
if (commentEnd <= change.op.p) {
historyComments.push(historyComment)
} else {
newPendingComments.push(historyComment)
}
}
pendingComments = newPendingComments
// The rest of pending comments overlap with this tracked change. Adjust
// their history length.
for (const historyComment of pendingComments) {
historyComment.op.hlen =
(historyComment.op.hlen ?? historyComment.op.c.length) +
change.op.d.length
}
// Adjust the offset
offset += change.op.d.length
}
// Save the last pending comments
for (const historyComment of pendingComments) {
historyComments.push(historyComment)
}
// Save any comments that came after the last tracked change
for (const comment of commentsIterator.nextComments()) {
historyComments.push(toHistoryComment(comment, offset))
}
const historyRanges = {}
if (historyComments.length > 0) {
historyRanges.comments = historyComments
}
if (historyChanges.length > 0) {
historyRanges.changes = historyChanges
}
return historyRanges
}
class CommentsIterator {
/**
* Build a CommentsIterator
*
* @param {Comment[]} comments
*/
constructor(comments) {
this.comments = comments
this.currentIndex = 0
}
/**
* Generator that returns the next comments to consider
*
* @param {number} beforePos - only return comments that start before this position
* @return {Iterable<Comment>}
*/
*nextComments(beforePos = Infinity) {
while (this.currentIndex < this.comments.length) {
const comment = this.comments[this.currentIndex]
if (comment.op.p < beforePos) {
yield comment
this.currentIndex += 1
} else {
return
}
}
}
}
/**
* Convert an editor tracked change into a history tracked change
*
* @param {TrackedChange} change
* @param {number} offset - how much the history change is ahead of the
* editor change
* @return {HistoryTrackedChange}
*/
function toHistoryChange(change, offset) {
/** @type {HistoryTrackedChange} */
const historyChange = _.cloneDeep(change)
if (offset > 0) {
historyChange.op.hpos = change.op.p + offset
}
return historyChange
}
/**
* Convert an editor comment into a history comment
*
* @param {Comment} comment
* @param {number} offset - how much the history comment is ahead of the
* editor comment
* @return {HistoryComment}
*/
function toHistoryComment(comment, offset) {
/** @type {HistoryComment} */
const historyComment = _.cloneDeep(comment)
if (offset > 0) {
historyComment.op.hpos = comment.op.p + offset
}
return historyComment
}
module.exports = {
toHistoryRanges,
}