text_operation.js 19 KB

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