mirror of
https://github.com/overleaf/overleaf.git
synced 2024-12-29 14:14:04 +00:00
202 lines
5.5 KiB
JavaScript
202 lines
5.5 KiB
JavaScript
|
import _ from 'lodash'
|
||
|
import Core from 'overleaf-editor-core'
|
||
|
import * as Errors from './Errors.js'
|
||
|
import * as OperationsCompressor from './OperationsCompressor.js'
|
||
|
|
||
|
export function convertToChanges(projectId, updatesWithBlobs, callback) {
|
||
|
let changes
|
||
|
try {
|
||
|
// convert update to change
|
||
|
changes = updatesWithBlobs.map(update =>
|
||
|
_convertToChange(projectId, update)
|
||
|
)
|
||
|
} catch (error1) {
|
||
|
const error = error1
|
||
|
if (
|
||
|
error instanceof Errors.UpdateWithUnknownFormatError ||
|
||
|
error instanceof Errors.UnexpectedOpTypeError
|
||
|
) {
|
||
|
return callback(error)
|
||
|
} else {
|
||
|
throw error
|
||
|
}
|
||
|
}
|
||
|
|
||
|
callback(null, changes)
|
||
|
}
|
||
|
|
||
|
function _convertToChange(projectId, updateWithBlob) {
|
||
|
let operations
|
||
|
const { update } = updateWithBlob
|
||
|
|
||
|
let projectVersion = null
|
||
|
const v2DocVersions = {}
|
||
|
|
||
|
if (_isRenameUpdate(update)) {
|
||
|
operations = [
|
||
|
{
|
||
|
pathname: _convertPathname(update.pathname),
|
||
|
newPathname: _convertPathname(update.new_pathname),
|
||
|
},
|
||
|
]
|
||
|
projectVersion = update.version
|
||
|
} else if (isAddUpdate(update)) {
|
||
|
operations = [
|
||
|
{
|
||
|
pathname: _convertPathname(update.pathname),
|
||
|
file: {
|
||
|
hash: updateWithBlob.blobHash,
|
||
|
},
|
||
|
},
|
||
|
]
|
||
|
projectVersion = update.version
|
||
|
} else if (isTextUpdate(update)) {
|
||
|
const docLength = update.meta.doc_length
|
||
|
let pathname = update.meta.pathname
|
||
|
|
||
|
pathname = _convertPathname(pathname)
|
||
|
const builder = new TextOperationsBuilder(update.doc, docLength, pathname)
|
||
|
// convert ops
|
||
|
for (const op of update.op) {
|
||
|
builder.addOp(op)
|
||
|
} // if this throws an exception it will be caught in convertToChanges
|
||
|
operations = builder.finish()
|
||
|
// add doc version information if present
|
||
|
if (update.v != null) {
|
||
|
v2DocVersions[update.doc] = { pathname, v: update.v }
|
||
|
}
|
||
|
} else {
|
||
|
const error = new Errors.UpdateWithUnknownFormatError(
|
||
|
'update with unknown format',
|
||
|
{ projectId, update }
|
||
|
)
|
||
|
throw error
|
||
|
}
|
||
|
|
||
|
const rawChange = {
|
||
|
operations,
|
||
|
v2Authors: _.compact([update.meta.user_id]),
|
||
|
timestamp: new Date(update.meta.ts).toISOString(),
|
||
|
projectVersion,
|
||
|
v2DocVersions: Object.keys(v2DocVersions).length ? v2DocVersions : null,
|
||
|
}
|
||
|
if (update.meta.origin) {
|
||
|
rawChange.origin = update.meta.origin
|
||
|
} else if (update.meta.type === 'external' && update.meta.source) {
|
||
|
rawChange.origin = { kind: update.meta.source }
|
||
|
}
|
||
|
const change = Core.Change.fromRaw(rawChange)
|
||
|
|
||
|
change.operations = OperationsCompressor.compressOperations(change.operations)
|
||
|
|
||
|
return change
|
||
|
}
|
||
|
|
||
|
function _isRenameUpdate(update) {
|
||
|
return update.new_pathname != null
|
||
|
}
|
||
|
|
||
|
function _isAddDocUpdate(update) {
|
||
|
return update.doc != null && update.docLines != null
|
||
|
}
|
||
|
|
||
|
function _isAddFileUpdate(update) {
|
||
|
return update.file != null && update.url != null
|
||
|
}
|
||
|
|
||
|
export function isTextUpdate(update) {
|
||
|
return (
|
||
|
update.doc != null &&
|
||
|
update.op != null &&
|
||
|
update.meta.pathname != null &&
|
||
|
update.meta.doc_length != null
|
||
|
)
|
||
|
}
|
||
|
|
||
|
export function isProjectStructureUpdate(update) {
|
||
|
return isAddUpdate(update) || _isRenameUpdate(update)
|
||
|
}
|
||
|
|
||
|
export function isAddUpdate(update) {
|
||
|
return _isAddDocUpdate(update) || _isAddFileUpdate(update)
|
||
|
}
|
||
|
|
||
|
export function _convertPathname(pathname) {
|
||
|
// Strip leading /
|
||
|
pathname = pathname.replace(/^\//, '')
|
||
|
// Replace \\ with _. Backslashes are no longer allowed
|
||
|
// in projects in web, but we have some which have gone through
|
||
|
// into history before this restriction was added. This makes
|
||
|
// them valid for the history store.
|
||
|
// See https://github.com/overleaf/write_latex/issues/4471
|
||
|
pathname = pathname.replace(/\\/g, '_')
|
||
|
// workaround for filenames containing asterisks, this will
|
||
|
// fail if a corresponding replacement file already exists but it
|
||
|
// would fail anyway without this attempt to fix the pathname.
|
||
|
// See https://github.com/overleaf/sharelatex/issues/900
|
||
|
pathname = pathname.replace(/\*/g, '__ASTERISK__')
|
||
|
// workaround for filenames beginning with spaces
|
||
|
// See https://github.com/overleaf/sharelatex/issues/1404
|
||
|
// note: we have already stripped any leading slash above
|
||
|
pathname = pathname.replace(/^ /, '__SPACE__') // handle top-level
|
||
|
pathname = pathname.replace(/\/ /g, '/__SPACE__') // handle folders
|
||
|
return pathname
|
||
|
}
|
||
|
|
||
|
class TextOperationsBuilder {
|
||
|
constructor(docId, docLength, pathname) {
|
||
|
this.operations = []
|
||
|
this.doc_id = docId
|
||
|
this.doc_length = docLength
|
||
|
this.pathname = pathname
|
||
|
}
|
||
|
|
||
|
addOp(op) {
|
||
|
let retain
|
||
|
if (op.c != null) {
|
||
|
return // ignore comment op
|
||
|
}
|
||
|
if (op.i == null && op.d == null) {
|
||
|
throw new Errors.UnexpectedOpTypeError('unexpected op type', { op })
|
||
|
}
|
||
|
|
||
|
// We sometimes receive operations that operate at positions outside the
|
||
|
// doc_length. Document updater coerces the position to the end of the
|
||
|
// document. We do the same here.
|
||
|
const pos = Math.min(op.p, this.doc_length)
|
||
|
|
||
|
const textOperation = []
|
||
|
if (pos > 0) {
|
||
|
textOperation.push(pos)
|
||
|
}
|
||
|
|
||
|
if (op.i != null) {
|
||
|
textOperation.push(op.i)
|
||
|
retain = this.doc_length - pos
|
||
|
this.doc_length += op.i.length
|
||
|
}
|
||
|
|
||
|
if (op.d != null) {
|
||
|
textOperation.push(-op.d.length)
|
||
|
retain = this.doc_length - pos - op.d.length
|
||
|
this.doc_length -= op.d.length
|
||
|
}
|
||
|
|
||
|
if (retain > 0) {
|
||
|
textOperation.push(retain)
|
||
|
}
|
||
|
this.pushTextOperation(textOperation)
|
||
|
}
|
||
|
|
||
|
pushTextOperation(textOperation) {
|
||
|
this.operations.push({
|
||
|
pathname: this.pathname,
|
||
|
textOperation,
|
||
|
})
|
||
|
}
|
||
|
|
||
|
finish() {
|
||
|
return this.operations
|
||
|
}
|
||
|
}
|