mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-11-12 21:23:45 +01:00
279 lines
7.3 KiB
JavaScript
279 lines
7.3 KiB
JavaScript
// @ts-check
|
|
|
|
const _ = require('lodash')
|
|
const RangesTracker = require('@overleaf/ranges-tracker')
|
|
const {
|
|
File,
|
|
StringFileData,
|
|
getDocUpdaterCompatibleRanges,
|
|
} = require('overleaf-editor-core')
|
|
const { addTrackedDeletesToContent, isDelete } = require('./Utils')
|
|
|
|
/**
|
|
* @import { Comment, HistoryComment, HistoryRanges, HistoryTrackedChange } from './types'
|
|
* @import { Ranges, TrackedChange } from './types'
|
|
* @import { CommentRawData, StringFileRawData, TrackedChangeRawData } from 'overleaf-editor-core/lib/types'
|
|
*/
|
|
|
|
/**
|
|
* 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
|
|
}
|
|
|
|
/**
|
|
* Convert a history-OT doc into editor lines and ranges
|
|
*
|
|
* Tracked deletes are removed from the content and the ranges use editor
|
|
* positions (as maintained by the RangesTracker).
|
|
*
|
|
* @param {StringFileRawData} raw
|
|
* @return {{lines: string[], ranges: Ranges}}
|
|
*/
|
|
function fromHistoryOT(raw) {
|
|
const fileData = StringFileData.fromRaw(raw)
|
|
const { changes, comments } = getDocUpdaterCompatibleRanges(
|
|
new File(fileData)
|
|
)
|
|
/** @type {Ranges} */
|
|
const ranges = {}
|
|
if (changes.length > 0) {
|
|
ranges.changes = changes.map(change => ({
|
|
...change,
|
|
id: RangesTracker.generateId(),
|
|
}))
|
|
}
|
|
if (comments.length > 0) {
|
|
ranges.comments = comments.map(comment => ({
|
|
...comment,
|
|
id: comment.op.t,
|
|
}))
|
|
}
|
|
return { lines: fileData.getLines(), ranges }
|
|
}
|
|
|
|
/**
|
|
* Convert editor lines and ranges into a history-OT doc
|
|
*
|
|
* Tracked deletes are added back to the content and the ranges use history
|
|
* positions.
|
|
*
|
|
* @param {string[]} lines
|
|
* @param {Ranges} ranges
|
|
* @param {string[]} resolvedCommentIds
|
|
* @return {StringFileRawData}
|
|
*/
|
|
function toHistoryOT(lines, ranges, resolvedCommentIds) {
|
|
const historyRanges = toHistoryRanges(ranges)
|
|
|
|
/** @type {StringFileRawData} */
|
|
const raw = {
|
|
content: addTrackedDeletesToContent(lines.join('\n'), ranges.changes ?? []),
|
|
}
|
|
|
|
const comments = (historyRanges.comments ?? []).map(comment => {
|
|
const length = comment.op.hlen ?? comment.op.c.length
|
|
/** @type {CommentRawData} */
|
|
const rawComment = {
|
|
id: comment.op.t,
|
|
// Translate zero length comments into detached comments
|
|
ranges:
|
|
length > 0 ? [{ pos: comment.op.hpos ?? comment.op.p, length }] : [],
|
|
}
|
|
if (resolvedCommentIds.includes(comment.op.t)) {
|
|
rawComment.resolved = true
|
|
}
|
|
return rawComment
|
|
})
|
|
if (comments.length > 0) {
|
|
raw.comments = comments
|
|
}
|
|
|
|
const trackedChanges = (historyRanges.changes ?? []).map(change => {
|
|
/** @type {TrackedChangeRawData} */
|
|
const trackedChange = {
|
|
range: {
|
|
pos: change.op.hpos ?? change.op.p,
|
|
length: isDelete(change.op) ? change.op.d.length : change.op.i.length,
|
|
},
|
|
tracking: {
|
|
type: isDelete(change.op) ? 'delete' : 'insert',
|
|
userId: change.metadata.user_id,
|
|
ts: new Date(change.metadata.ts).toISOString(),
|
|
},
|
|
}
|
|
return trackedChange
|
|
})
|
|
if (trackedChanges.length > 0) {
|
|
raw.trackedChanges = trackedChanges
|
|
}
|
|
|
|
return raw
|
|
}
|
|
|
|
module.exports = {
|
|
toHistoryRanges,
|
|
toHistoryOT,
|
|
fromHistoryOT,
|
|
}
|