text_operation.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711
  1. /**
  2. * The text operation from OT.js with some minor cosmetic changes.
  3. *
  4. * Specifically, this is based on
  5. * https://github.com/Operational-Transformation/ot.js/
  6. * blob/298825f58fb51fefb352e7df5ddbc668f4d5646f/lib/text-operation.js
  7. * from 18 Mar 2013.
  8. */
  9. 'use strict'
  10. const containsNonBmpChars = require('../util').containsNonBmpChars
  11. const OError = require('@overleaf/o-error')
  12. class UnprocessableError extends OError {}
  13. class ApplyError extends UnprocessableError {
  14. constructor(message, operation, operand) {
  15. super(message, { operation, operand })
  16. this.operation = operation
  17. this.operand = operand
  18. }
  19. }
  20. class InvalidInsertionError extends UnprocessableError {
  21. constructor(str, operation) {
  22. super('inserted text contains non BMP characters', { str, operation })
  23. this.str = str
  24. this.operation = operation
  25. }
  26. }
  27. class TooLongError extends UnprocessableError {
  28. constructor(operation, resultLength) {
  29. super(`resulting string would be too long: ${resultLength}`, {
  30. operation,
  31. resultLength,
  32. })
  33. this.operation = operation
  34. this.resultLength = resultLength
  35. }
  36. }
  37. /**
  38. * Create an empty text operation.
  39. */
  40. class TextOperation {
  41. /**
  42. * Length of the longest file that we'll attempt to edit, in characters.
  43. *
  44. * @type {number}
  45. */
  46. static MAX_STRING_LENGTH = 2 * Math.pow(1024, 2)
  47. static UnprocessableError = UnprocessableError
  48. static ApplyError = ApplyError
  49. static InvalidInsertionError = InvalidInsertionError
  50. static TooLongError = TooLongError
  51. static isRetain = isRetain
  52. static isInsert = isInsert
  53. static isRemove = isRemove
  54. constructor() {
  55. // When an operation is applied to an input string, you can think of this as
  56. // if an imaginary cursor runs over the entire string and skips over some
  57. // parts, removes some parts and inserts characters at some positions. These
  58. // actions (skip/remove/insert) are stored as an array in the "ops" property.
  59. this.ops = []
  60. // An operation's baseLength is the length of every string the operation
  61. // can be applied to.
  62. this.baseLength = 0
  63. // The targetLength is the length of every string that results from applying
  64. // the operation on a valid input string.
  65. this.targetLength = 0
  66. }
  67. equals(other) {
  68. if (this.baseLength !== other.baseLength) {
  69. return false
  70. }
  71. if (this.targetLength !== other.targetLength) {
  72. return false
  73. }
  74. if (this.ops.length !== other.ops.length) {
  75. return false
  76. }
  77. for (let i = 0; i < this.ops.length; i++) {
  78. if (this.ops[i] !== other.ops[i]) {
  79. return false
  80. }
  81. }
  82. return true
  83. }
  84. // After an operation is constructed, the user of the library can specify the
  85. // actions of an operation (skip/insert/remove) with these three builder
  86. // methods. They all return the operation for convenient chaining.
  87. /**
  88. * Skip over a given number of characters.
  89. */
  90. retain(n) {
  91. if (typeof n !== 'number') {
  92. throw new Error('retain expects an integer')
  93. }
  94. if (n === 0) {
  95. return this
  96. }
  97. this.baseLength += n
  98. this.targetLength += n
  99. if (isRetain(this.ops[this.ops.length - 1])) {
  100. // The last op is a retain op => we can merge them into one op.
  101. this.ops[this.ops.length - 1] += n
  102. } else {
  103. // Create a new op.
  104. this.ops.push(n)
  105. }
  106. return this
  107. }
  108. /**
  109. * Insert a string at the current position.
  110. */
  111. insert(str) {
  112. if (typeof str !== 'string') {
  113. throw new Error('insert expects a string')
  114. }
  115. if (containsNonBmpChars(str)) {
  116. throw new TextOperation.InvalidInsertionError(str)
  117. }
  118. if (str === '') {
  119. return this
  120. }
  121. this.targetLength += str.length
  122. const ops = this.ops
  123. if (isInsert(ops[ops.length - 1])) {
  124. // Merge insert op.
  125. ops[ops.length - 1] += str
  126. } else if (isRemove(ops[ops.length - 1])) {
  127. // It doesn't matter when an operation is applied whether the operation
  128. // is remove(3), insert("something") or insert("something"), remove(3).
  129. // Here we enforce that in this case, the insert op always comes first.
  130. // This makes all operations that have the same effect when applied to
  131. // a document of the right length equal in respect to the `equals` method.
  132. if (isInsert(ops[ops.length - 2])) {
  133. ops[ops.length - 2] += str
  134. } else {
  135. ops[ops.length] = ops[ops.length - 1]
  136. ops[ops.length - 2] = str
  137. }
  138. } else {
  139. ops.push(str)
  140. }
  141. return this
  142. }
  143. /**
  144. * Remove a string at the current position.
  145. */
  146. remove(n) {
  147. if (typeof n === 'string') {
  148. n = n.length
  149. }
  150. if (typeof n !== 'number') {
  151. throw new Error('remove expects an integer or a string')
  152. }
  153. if (n === 0) {
  154. return this
  155. }
  156. if (n > 0) {
  157. n = -n
  158. }
  159. this.baseLength -= n
  160. if (isRemove(this.ops[this.ops.length - 1])) {
  161. this.ops[this.ops.length - 1] += n
  162. } else {
  163. this.ops.push(n)
  164. }
  165. return this
  166. }
  167. /**
  168. * Tests whether this operation has no effect.
  169. */
  170. isNoop() {
  171. return (
  172. this.ops.length === 0 || (this.ops.length === 1 && isRetain(this.ops[0]))
  173. )
  174. }
  175. /**
  176. * Pretty printing.
  177. */
  178. toString() {
  179. return this.ops
  180. .map(op => {
  181. if (isRetain(op)) {
  182. return 'retain ' + op
  183. } else if (isInsert(op)) {
  184. return "insert '" + op + "'"
  185. } else {
  186. return 'remove ' + -op
  187. }
  188. })
  189. .join(', ')
  190. }
  191. /**
  192. * Converts operation into a JSON value.
  193. */
  194. toJSON() {
  195. return this.ops
  196. }
  197. /**
  198. * Converts a plain JS object into an operation and validates it.
  199. */
  200. static fromJSON = function (ops) {
  201. const o = new TextOperation()
  202. for (let i = 0, l = ops.length; i < l; i++) {
  203. const op = ops[i]
  204. if (isRetain(op)) {
  205. o.retain(op)
  206. } else if (isInsert(op)) {
  207. o.insert(op)
  208. } else if (isRemove(op)) {
  209. o.remove(op)
  210. } else {
  211. throw new Error(
  212. 'unknown operation: ' +
  213. JSON.stringify(op) +
  214. ' in ' +
  215. JSON.stringify(ops)
  216. )
  217. }
  218. }
  219. return o
  220. }
  221. /**
  222. * Apply an operation to a string, returning a new string. Throws an error if
  223. * there's a mismatch between the input string and the operation.
  224. */
  225. apply(str) {
  226. const operation = this
  227. if (containsNonBmpChars(str)) {
  228. throw new TextOperation.ApplyError(
  229. 'The string contains non BMP characters.',
  230. operation,
  231. str
  232. )
  233. }
  234. if (str.length !== operation.baseLength) {
  235. throw new TextOperation.ApplyError(
  236. "The operation's base length must be equal to the string's length.",
  237. operation,
  238. str
  239. )
  240. }
  241. // Build up the result string directly by concatenation (which is actually
  242. // faster than joining arrays because it is optimised in v8).
  243. let result = ''
  244. let strIndex = 0
  245. const ops = this.ops
  246. for (let i = 0, l = ops.length; i < l; i++) {
  247. const op = ops[i]
  248. if (isRetain(op)) {
  249. if (strIndex + op > str.length) {
  250. throw new TextOperation.ApplyError(
  251. "Operation can't retain more chars than are left in the string.",
  252. operation,
  253. str
  254. )
  255. }
  256. // Copy skipped part of the old string.
  257. result += str.slice(strIndex, strIndex + op)
  258. strIndex += op
  259. } else if (isInsert(op)) {
  260. if (containsNonBmpChars(op)) {
  261. throw new TextOperation.InvalidInsertionError(str, operation)
  262. }
  263. // Insert string.
  264. result += op
  265. } else {
  266. // remove op
  267. strIndex -= op
  268. }
  269. }
  270. if (strIndex !== str.length) {
  271. throw new TextOperation.ApplyError(
  272. "The operation didn't operate on the whole string.",
  273. operation,
  274. str
  275. )
  276. }
  277. if (result.length > TextOperation.MAX_STRING_LENGTH) {
  278. throw new TextOperation.TooLongError(operation, result.length)
  279. }
  280. return result
  281. }
  282. /**
  283. * Determine the effect of this operation on the length of the text.
  284. *
  285. * NB: This is an Overleaf addition to the original TextOperation.
  286. *
  287. * @param {number} length of the original string; non-negative
  288. * @return {number} length of the new string; non-negative
  289. */
  290. applyToLength(length) {
  291. const operation = this
  292. if (length !== operation.baseLength) {
  293. throw new TextOperation.ApplyError(
  294. "The operation's base length must be equal to the string's length.",
  295. operation,
  296. length
  297. )
  298. }
  299. let newLength = 0
  300. let strIndex = 0
  301. const ops = this.ops
  302. for (let i = 0, l = ops.length; i < l; i++) {
  303. const op = ops[i]
  304. if (isRetain(op)) {
  305. if (strIndex + op > length) {
  306. throw new TextOperation.ApplyError(
  307. "Operation can't retain more chars than are left in the string.",
  308. operation,
  309. length
  310. )
  311. }
  312. // Copy skipped part of the old string.
  313. newLength += op
  314. strIndex += op
  315. } else if (isInsert(op)) {
  316. // Insert string.
  317. newLength += op.length
  318. } else {
  319. // remove op
  320. strIndex -= op
  321. }
  322. }
  323. if (strIndex !== length) {
  324. throw new TextOperation.ApplyError(
  325. "The operation didn't operate on the whole string.",
  326. operation,
  327. length
  328. )
  329. }
  330. if (newLength > TextOperation.MAX_STRING_LENGTH) {
  331. throw new TextOperation.TooLongError(operation, newLength)
  332. }
  333. return newLength
  334. }
  335. /**
  336. * Computes the inverse of an operation. The inverse of an operation is the
  337. * operation that reverts the effects of the operation, e.g. when you have an
  338. * operation 'insert("hello "); skip(6);' then the inverse is 'remove("hello ");
  339. * skip(6);'. The inverse should be used for implementing undo.
  340. */
  341. invert(str) {
  342. let strIndex = 0
  343. const inverse = new TextOperation()
  344. const ops = this.ops
  345. for (let i = 0, l = ops.length; i < l; i++) {
  346. const op = ops[i]
  347. if (isRetain(op)) {
  348. inverse.retain(op)
  349. strIndex += op
  350. } else if (isInsert(op)) {
  351. inverse.remove(op.length)
  352. } else {
  353. // remove op
  354. inverse.insert(str.slice(strIndex, strIndex - op))
  355. strIndex -= op
  356. }
  357. }
  358. return inverse
  359. }
  360. /**
  361. * When you use ctrl-z to undo your latest changes, you expect the program not
  362. * to undo every single keystroke but to undo your last sentence you wrote at
  363. * a stretch or the deletion you did by holding the backspace key down. This
  364. * This can be implemented by composing operations on the undo stack. This
  365. * method can help decide whether two operations should be composed. It
  366. * returns true if the operations are consecutive insert operations or both
  367. * operations delete text at the same position. You may want to include other
  368. * factors like the time since the last change in your decision.
  369. */
  370. canBeComposedWithForUndo(other) {
  371. if (this.isNoop() || other.isNoop()) {
  372. return true
  373. }
  374. const startA = getStartIndex(this)
  375. const startB = getStartIndex(other)
  376. const simpleA = getSimpleOp(this)
  377. const simpleB = getSimpleOp(other)
  378. if (!simpleA || !simpleB) {
  379. return false
  380. }
  381. if (isInsert(simpleA) && isInsert(simpleB)) {
  382. return startA + simpleA.length === startB
  383. }
  384. if (isRemove(simpleA) && isRemove(simpleB)) {
  385. // there are two possibilities to delete: with backspace and with the
  386. // delete key.
  387. return startB - simpleB === startA || startA === startB
  388. }
  389. return false
  390. }
  391. /**
  392. * @inheritdoc
  393. */
  394. canBeComposedWith(other) {
  395. return this.targetLength === other.baseLength
  396. }
  397. // Compose merges two consecutive operations into one operation, that
  398. // preserves the changes of both. Or, in other words, for each input string S
  399. // and a pair of consecutive operations A and B,
  400. // apply(apply(S, A), B) = apply(S, compose(A, B)) must hold.
  401. compose(operation2) {
  402. const operation1 = this
  403. if (operation1.targetLength !== operation2.baseLength) {
  404. throw new Error(
  405. 'The base length of the second operation has to be the ' +
  406. 'target length of the first operation'
  407. )
  408. }
  409. const operation = new TextOperation() // the combined operation
  410. const ops1 = operation1.ops
  411. const ops2 = operation2.ops // for fast access
  412. let i1 = 0
  413. let i2 = 0 // current index into ops1 respectively ops2
  414. let op1 = ops1[i1++]
  415. let op2 = ops2[i2++] // current ops
  416. for (;;) {
  417. // Dispatch on the type of op1 and op2
  418. if (typeof op1 === 'undefined' && typeof op2 === 'undefined') {
  419. // end condition: both ops1 and ops2 have been processed
  420. break
  421. }
  422. if (isRemove(op1)) {
  423. operation.remove(op1)
  424. op1 = ops1[i1++]
  425. continue
  426. }
  427. if (isInsert(op2)) {
  428. operation.insert(op2)
  429. op2 = ops2[i2++]
  430. continue
  431. }
  432. if (typeof op1 === 'undefined') {
  433. throw new Error(
  434. 'Cannot compose operations: first operation is too short.'
  435. )
  436. }
  437. if (typeof op2 === 'undefined') {
  438. throw new Error(
  439. 'Cannot compose operations: first operation is too long.'
  440. )
  441. }
  442. if (isRetain(op1) && isRetain(op2)) {
  443. if (op1 > op2) {
  444. operation.retain(op2)
  445. op1 = op1 - op2
  446. op2 = ops2[i2++]
  447. } else if (op1 === op2) {
  448. operation.retain(op1)
  449. op1 = ops1[i1++]
  450. op2 = ops2[i2++]
  451. } else {
  452. operation.retain(op1)
  453. op2 = op2 - op1
  454. op1 = ops1[i1++]
  455. }
  456. } else if (isInsert(op1) && isRemove(op2)) {
  457. if (op1.length > -op2) {
  458. op1 = op1.slice(-op2)
  459. op2 = ops2[i2++]
  460. } else if (op1.length === -op2) {
  461. op1 = ops1[i1++]
  462. op2 = ops2[i2++]
  463. } else {
  464. op2 = op2 + op1.length
  465. op1 = ops1[i1++]
  466. }
  467. } else if (isInsert(op1) && isRetain(op2)) {
  468. if (op1.length > op2) {
  469. operation.insert(op1.slice(0, op2))
  470. op1 = op1.slice(op2)
  471. op2 = ops2[i2++]
  472. } else if (op1.length === op2) {
  473. operation.insert(op1)
  474. op1 = ops1[i1++]
  475. op2 = ops2[i2++]
  476. } else {
  477. operation.insert(op1)
  478. op2 = op2 - op1.length
  479. op1 = ops1[i1++]
  480. }
  481. } else if (isRetain(op1) && isRemove(op2)) {
  482. if (op1 > -op2) {
  483. operation.remove(op2)
  484. op1 = op1 + op2
  485. op2 = ops2[i2++]
  486. } else if (op1 === -op2) {
  487. operation.remove(op2)
  488. op1 = ops1[i1++]
  489. op2 = ops2[i2++]
  490. } else {
  491. operation.remove(op1)
  492. op2 = op2 + op1
  493. op1 = ops1[i1++]
  494. }
  495. } else {
  496. throw new Error(
  497. "This shouldn't happen: op1: " +
  498. JSON.stringify(op1) +
  499. ', op2: ' +
  500. JSON.stringify(op2)
  501. )
  502. }
  503. }
  504. return operation
  505. }
  506. /**
  507. * Transform takes two operations A and B that happened concurrently and
  508. * produces two operations A' and B' (in an array) such that
  509. * `apply(apply(S, A), B') = apply(apply(S, B), A')`. This function is the
  510. * heart of OT.
  511. */
  512. static transform(operation1, operation2) {
  513. if (operation1.baseLength !== operation2.baseLength) {
  514. throw new Error('Both operations have to have the same base length')
  515. }
  516. const operation1prime = new TextOperation()
  517. const operation2prime = new TextOperation()
  518. const ops1 = operation1.ops
  519. const ops2 = operation2.ops
  520. let i1 = 0
  521. let i2 = 0
  522. let op1 = ops1[i1++]
  523. let op2 = ops2[i2++]
  524. for (;;) {
  525. // At every iteration of the loop, the imaginary cursor that both
  526. // operation1 and operation2 have that operates on the input string must
  527. // have the same position in the input string.
  528. if (typeof op1 === 'undefined' && typeof op2 === 'undefined') {
  529. // end condition: both ops1 and ops2 have been processed
  530. break
  531. }
  532. // next two cases: one or both ops are insert ops
  533. // => insert the string in the corresponding prime operation, skip it in
  534. // the other one. If both op1 and op2 are insert ops, prefer op1.
  535. if (isInsert(op1)) {
  536. operation1prime.insert(op1)
  537. operation2prime.retain(op1.length)
  538. op1 = ops1[i1++]
  539. continue
  540. }
  541. if (isInsert(op2)) {
  542. operation1prime.retain(op2.length)
  543. operation2prime.insert(op2)
  544. op2 = ops2[i2++]
  545. continue
  546. }
  547. if (typeof op1 === 'undefined') {
  548. throw new Error(
  549. 'Cannot compose operations: first operation is too short.'
  550. )
  551. }
  552. if (typeof op2 === 'undefined') {
  553. throw new Error(
  554. 'Cannot compose operations: first operation is too long.'
  555. )
  556. }
  557. let minl
  558. if (isRetain(op1) && isRetain(op2)) {
  559. // Simple case: retain/retain
  560. if (op1 > op2) {
  561. minl = op2
  562. op1 = op1 - op2
  563. op2 = ops2[i2++]
  564. } else if (op1 === op2) {
  565. minl = op2
  566. op1 = ops1[i1++]
  567. op2 = ops2[i2++]
  568. } else {
  569. minl = op1
  570. op2 = op2 - op1
  571. op1 = ops1[i1++]
  572. }
  573. operation1prime.retain(minl)
  574. operation2prime.retain(minl)
  575. } else if (isRemove(op1) && isRemove(op2)) {
  576. // Both operations remove the same string at the same position. We don't
  577. // need to produce any operations, we just skip over the remove ops and
  578. // handle the case that one operation removes more than the other.
  579. if (-op1 > -op2) {
  580. op1 = op1 - op2
  581. op2 = ops2[i2++]
  582. } else if (op1 === op2) {
  583. op1 = ops1[i1++]
  584. op2 = ops2[i2++]
  585. } else {
  586. op2 = op2 - op1
  587. op1 = ops1[i1++]
  588. }
  589. // next two cases: remove/retain and retain/remove
  590. } else if (isRemove(op1) && isRetain(op2)) {
  591. if (-op1 > op2) {
  592. minl = op2
  593. op1 = op1 + op2
  594. op2 = ops2[i2++]
  595. } else if (-op1 === op2) {
  596. minl = op2
  597. op1 = ops1[i1++]
  598. op2 = ops2[i2++]
  599. } else {
  600. minl = -op1
  601. op2 = op2 + op1
  602. op1 = ops1[i1++]
  603. }
  604. operation1prime.remove(minl)
  605. } else if (isRetain(op1) && isRemove(op2)) {
  606. if (op1 > -op2) {
  607. minl = -op2
  608. op1 = op1 + op2
  609. op2 = ops2[i2++]
  610. } else if (op1 === -op2) {
  611. minl = op1
  612. op1 = ops1[i1++]
  613. op2 = ops2[i2++]
  614. } else {
  615. minl = op1
  616. op2 = op2 + op1
  617. op1 = ops1[i1++]
  618. }
  619. operation2prime.remove(minl)
  620. } else {
  621. throw new Error("The two operations aren't compatible")
  622. }
  623. }
  624. return [operation1prime, operation2prime]
  625. }
  626. }
  627. // Operation are essentially lists of ops. There are three types of ops:
  628. //
  629. // * Retain ops: Advance the cursor position by a given number of characters.
  630. // Represented by positive ints.
  631. // * Insert ops: Insert a given string at the current cursor position.
  632. // Represented by strings.
  633. // * Remove ops: Remove the next n characters. Represented by negative ints.
  634. function isRetain(op) {
  635. return typeof op === 'number' && op > 0
  636. }
  637. function isInsert(op) {
  638. return typeof op === 'string'
  639. }
  640. function isRemove(op) {
  641. return typeof op === 'number' && op < 0
  642. }
  643. function getSimpleOp(operation, fn) {
  644. const ops = operation.ops
  645. switch (ops.length) {
  646. case 1:
  647. return ops[0]
  648. case 2:
  649. return isRetain(ops[0]) ? ops[1] : isRetain(ops[1]) ? ops[0] : null
  650. case 3:
  651. if (isRetain(ops[0]) && isRetain(ops[2])) {
  652. return ops[1]
  653. }
  654. }
  655. return null
  656. }
  657. function getStartIndex(operation) {
  658. if (isRetain(operation.ops[0])) {
  659. return operation.ops[0]
  660. }
  661. return 0
  662. }
  663. module.exports = TextOperation