text-tp2.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. /* eslint-disable
  2. no-cond-assign,
  3. no-return-assign,
  4. no-undef,
  5. no-unused-vars,
  6. */
  7. // TODO: This file was created by bulk-decaffeinate.
  8. // Fix any style issues and re-enable lint.
  9. /*
  10. * decaffeinate suggestions:
  11. * DS101: Remove unnecessary use of Array.from
  12. * DS102: Remove unnecessary code created because of implicit returns
  13. * DS103: Rewrite code to no longer use __guard__
  14. * DS205: Consider reworking code to avoid use of IIFEs
  15. * DS207: Consider shorter variations of null checks
  16. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  17. */
  18. // A TP2 implementation of text, following this spec:
  19. // http://code.google.com/p/lightwave/source/browse/trunk/experimental/ot/README
  20. //
  21. // A document is made up of a string and a set of tombstones inserted throughout
  22. // the string. For example, 'some ', (2 tombstones), 'string'.
  23. //
  24. // This is encoded in a document as: {s:'some string', t:[5, -2, 6]}
  25. //
  26. // Ops are lists of components which iterate over the whole document.
  27. // Components are either:
  28. // N: Skip N characters in the original document
  29. // {i:'str'}: Insert 'str' at the current position in the document
  30. // {i:N}: Insert N tombstones at the current position in the document
  31. // {d:N}: Delete (tombstone) N characters at the current position in the document
  32. //
  33. // Eg: [3, {i:'hi'}, 5, {d:8}]
  34. //
  35. // Snapshots are lists with characters and tombstones. Characters are stored in strings
  36. // and adjacent tombstones are flattened into numbers.
  37. //
  38. // Eg, the document: 'Hello .....world' ('.' denotes tombstoned (deleted) characters)
  39. // would be represented by a document snapshot of ['Hello ', 5, 'world']
  40. let append, appendDoc, takeDoc
  41. var type = {
  42. name: 'text-tp2',
  43. tp2: true,
  44. create() {
  45. return { charLength: 0, totalLength: 0, positionCache: [], data: [] }
  46. },
  47. serialize(doc) {
  48. if (!doc.data) {
  49. throw new Error('invalid doc snapshot')
  50. }
  51. return doc.data
  52. },
  53. deserialize(data) {
  54. const doc = type.create()
  55. doc.data = data
  56. for (const component of Array.from(data)) {
  57. if (typeof component === 'string') {
  58. doc.charLength += component.length
  59. doc.totalLength += component.length
  60. } else {
  61. doc.totalLength += component
  62. }
  63. }
  64. return doc
  65. },
  66. }
  67. const checkOp = function (op) {
  68. if (!Array.isArray(op)) {
  69. throw new Error('Op must be an array of components')
  70. }
  71. let last = null
  72. return (() => {
  73. const result = []
  74. for (const c of Array.from(op)) {
  75. if (typeof c === 'object') {
  76. if (c.i !== undefined) {
  77. if (
  78. (typeof c.i !== 'string' || !(c.i.length > 0)) &&
  79. (typeof c.i !== 'number' || !(c.i > 0))
  80. ) {
  81. throw new Error('Inserts must insert a string or a +ive number')
  82. }
  83. } else if (c.d !== undefined) {
  84. if (typeof c.d !== 'number' || !(c.d > 0)) {
  85. throw new Error('Deletes must be a +ive number')
  86. }
  87. } else {
  88. throw new Error('Operation component must define .i or .d')
  89. }
  90. } else {
  91. if (typeof c !== 'number') {
  92. throw new Error('Op components must be objects or numbers')
  93. }
  94. if (!(c > 0)) {
  95. throw new Error('Skip components must be a positive number')
  96. }
  97. if (typeof last === 'number') {
  98. throw new Error('Adjacent skip components should be combined')
  99. }
  100. }
  101. result.push((last = c))
  102. }
  103. return result
  104. })()
  105. }
  106. // Take the next part from the specified position in a document snapshot.
  107. // position = {index, offset}. It will be updated.
  108. type._takeDoc = takeDoc = function (
  109. doc,
  110. position,
  111. maxlength,
  112. tombsIndivisible
  113. ) {
  114. if (position.index >= doc.data.length) {
  115. throw new Error('Operation goes past the end of the document')
  116. }
  117. const part = doc.data[position.index]
  118. // peel off data[0]
  119. const result =
  120. typeof part === 'string'
  121. ? maxlength !== undefined
  122. ? part.slice(position.offset, position.offset + maxlength)
  123. : part.slice(position.offset)
  124. : maxlength === undefined || tombsIndivisible
  125. ? part - position.offset
  126. : Math.min(maxlength, part - position.offset)
  127. const resultLen = result.length || result
  128. if ((part.length || part) - position.offset > resultLen) {
  129. position.offset += resultLen
  130. } else {
  131. position.index++
  132. position.offset = 0
  133. }
  134. return result
  135. }
  136. // Append a part to the end of a document
  137. type._appendDoc = appendDoc = function (doc, p) {
  138. if (p === 0 || p === '') {
  139. return
  140. }
  141. if (typeof p === 'string') {
  142. doc.charLength += p.length
  143. doc.totalLength += p.length
  144. } else {
  145. doc.totalLength += p
  146. }
  147. const { data } = doc
  148. if (data.length === 0) {
  149. data.push(p)
  150. } else if (typeof data[data.length - 1] === typeof p) {
  151. data[data.length - 1] += p
  152. } else {
  153. data.push(p)
  154. }
  155. }
  156. // Apply the op to the document. The document is not modified in the process.
  157. type.apply = function (doc, op) {
  158. if (
  159. doc.totalLength === undefined ||
  160. doc.charLength === undefined ||
  161. doc.data.length === undefined
  162. ) {
  163. throw new Error('Snapshot is invalid')
  164. }
  165. checkOp(op)
  166. const newDoc = type.create()
  167. const position = { index: 0, offset: 0 }
  168. for (const component of Array.from(op)) {
  169. var part, remainder
  170. if (typeof component === 'number') {
  171. remainder = component
  172. while (remainder > 0) {
  173. part = takeDoc(doc, position, remainder)
  174. appendDoc(newDoc, part)
  175. remainder -= part.length || part
  176. }
  177. } else if (component.i !== undefined) {
  178. appendDoc(newDoc, component.i)
  179. } else if (component.d !== undefined) {
  180. remainder = component.d
  181. while (remainder > 0) {
  182. part = takeDoc(doc, position, remainder)
  183. remainder -= part.length || part
  184. }
  185. appendDoc(newDoc, component.d)
  186. }
  187. }
  188. return newDoc
  189. }
  190. // Append an op component to the end of the specified op.
  191. // Exported for the randomOpGenerator.
  192. type._append = append = function (op, component) {
  193. if (
  194. component === 0 ||
  195. component.i === '' ||
  196. component.i === 0 ||
  197. component.d === 0
  198. ) {
  199. } else if (op.length === 0) {
  200. return op.push(component)
  201. } else {
  202. const last = op[op.length - 1]
  203. if (typeof component === 'number' && typeof last === 'number') {
  204. return (op[op.length - 1] += component)
  205. } else if (
  206. component.i !== undefined &&
  207. last.i != null &&
  208. typeof last.i === typeof component.i
  209. ) {
  210. return (last.i += component.i)
  211. } else if (component.d !== undefined && last.d != null) {
  212. return (last.d += component.d)
  213. } else {
  214. return op.push(component)
  215. }
  216. }
  217. }
  218. // Makes 2 functions for taking components from the start of an op, and for peeking
  219. // at the next op that could be taken.
  220. const makeTake = function (op) {
  221. // The index of the next component to take
  222. let index = 0
  223. // The offset into the component
  224. let offset = 0
  225. // Take up to length maxlength from the op. If maxlength is not defined, there is no max.
  226. // If insertsIndivisible is true, inserts (& insert tombstones) won't be separated.
  227. //
  228. // Returns null when op is fully consumed.
  229. const take = function (maxlength, insertsIndivisible) {
  230. let current
  231. if (index === op.length) {
  232. return null
  233. }
  234. const e = op[index]
  235. if (
  236. typeof (current = e) === 'number' ||
  237. typeof (current = e.i) === 'number' ||
  238. (current = e.d) !== undefined
  239. ) {
  240. let c
  241. if (
  242. maxlength == null ||
  243. current - offset <= maxlength ||
  244. (insertsIndivisible && e.i !== undefined)
  245. ) {
  246. // Return the rest of the current element.
  247. c = current - offset
  248. ++index
  249. offset = 0
  250. } else {
  251. offset += maxlength
  252. c = maxlength
  253. }
  254. if (e.i !== undefined) {
  255. return { i: c }
  256. } else if (e.d !== undefined) {
  257. return { d: c }
  258. } else {
  259. return c
  260. }
  261. } else {
  262. // Take from the inserted string
  263. let result
  264. if (
  265. maxlength == null ||
  266. e.i.length - offset <= maxlength ||
  267. insertsIndivisible
  268. ) {
  269. result = { i: e.i.slice(offset) }
  270. ++index
  271. offset = 0
  272. } else {
  273. result = { i: e.i.slice(offset, offset + maxlength) }
  274. offset += maxlength
  275. }
  276. return result
  277. }
  278. }
  279. const peekType = () => op[index]
  280. return [take, peekType]
  281. }
  282. // Find and return the length of an op component
  283. const componentLength = function (component) {
  284. if (typeof component === 'number') {
  285. return component
  286. } else if (typeof component.i === 'string') {
  287. return component.i.length
  288. } else {
  289. // This should work because c.d and c.i must be +ive.
  290. return component.d || component.i
  291. }
  292. }
  293. // Normalize an op, removing all empty skips and empty inserts / deletes. Concatenate
  294. // adjacent inserts and deletes.
  295. type.normalize = function (op) {
  296. const newOp = []
  297. for (const component of Array.from(op)) {
  298. append(newOp, component)
  299. }
  300. return newOp
  301. }
  302. // This is a helper method to transform and prune. goForwards is true for transform, false for prune.
  303. const transformer = function (op, otherOp, goForwards, side) {
  304. let component
  305. checkOp(op)
  306. checkOp(otherOp)
  307. const newOp = []
  308. const [take, peek] = Array.from(makeTake(op))
  309. for (component of Array.from(otherOp)) {
  310. var chunk
  311. let length = componentLength(component)
  312. if (component.i !== undefined) {
  313. // Insert text or tombs
  314. if (goForwards) {
  315. // transform - insert skips over inserted parts
  316. if (side === 'left') {
  317. // The left insert should go first.
  318. while (__guard__(peek(), x => x.i) !== undefined) {
  319. append(newOp, take())
  320. }
  321. }
  322. // In any case, skip the inserted text.
  323. append(newOp, length)
  324. } else {
  325. // Prune. Remove skips for inserts.
  326. while (length > 0) {
  327. chunk = take(length, true)
  328. if (chunk === null) {
  329. throw new Error('The transformed op is invalid')
  330. }
  331. if (chunk.d !== undefined) {
  332. throw new Error(
  333. 'The transformed op deletes locally inserted characters - it cannot be purged of the insert.'
  334. )
  335. }
  336. if (typeof chunk === 'number') {
  337. length -= chunk
  338. } else {
  339. append(newOp, chunk)
  340. }
  341. }
  342. }
  343. } else {
  344. // Skip or delete
  345. while (length > 0) {
  346. chunk = take(length, true)
  347. if (chunk === null) {
  348. throw new Error(
  349. 'The op traverses more elements than the document has'
  350. )
  351. }
  352. append(newOp, chunk)
  353. if (!chunk.i) {
  354. length -= componentLength(chunk)
  355. }
  356. }
  357. }
  358. }
  359. // Append extras from op1
  360. while ((component = take())) {
  361. if (component.i === undefined) {
  362. throw new Error(`Remaining fragments in the op: ${component}`)
  363. }
  364. append(newOp, component)
  365. }
  366. return newOp
  367. }
  368. // transform op1 by op2. Return transformed version of op1.
  369. // op1 and op2 are unchanged by transform.
  370. // side should be 'left' or 'right', depending on if op1.id <> op2.id. 'left' == client op.
  371. type.transform = function (op, otherOp, side) {
  372. if (side !== 'left' && side !== 'right') {
  373. throw new Error(`side (${side}) should be 'left' or 'right'`)
  374. }
  375. return transformer(op, otherOp, true, side)
  376. }
  377. // Prune is the inverse of transform.
  378. type.prune = (op, otherOp) => transformer(op, otherOp, false)
  379. // Compose 2 ops into 1 op.
  380. type.compose = function (op1, op2) {
  381. let component
  382. if (op1 === null || op1 === undefined) {
  383. return op2
  384. }
  385. checkOp(op1)
  386. checkOp(op2)
  387. const result = []
  388. const [take, _] = Array.from(makeTake(op1))
  389. for (component of Array.from(op2)) {
  390. var chunk, length
  391. if (typeof component === 'number') {
  392. // Skip
  393. // Just copy from op1.
  394. length = component
  395. while (length > 0) {
  396. chunk = take(length)
  397. if (chunk === null) {
  398. throw new Error(
  399. 'The op traverses more elements than the document has'
  400. )
  401. }
  402. append(result, chunk)
  403. length -= componentLength(chunk)
  404. }
  405. } else if (component.i !== undefined) {
  406. // Insert
  407. append(result, { i: component.i })
  408. } else {
  409. // Delete
  410. length = component.d
  411. while (length > 0) {
  412. chunk = take(length)
  413. if (chunk === null) {
  414. throw new Error(
  415. 'The op traverses more elements than the document has'
  416. )
  417. }
  418. const chunkLength = componentLength(chunk)
  419. if (chunk.i !== undefined) {
  420. append(result, { i: chunkLength })
  421. } else {
  422. append(result, { d: chunkLength })
  423. }
  424. length -= chunkLength
  425. }
  426. }
  427. }
  428. // Append extras from op1
  429. while ((component = take())) {
  430. if (component.i === undefined) {
  431. throw new Error(`Remaining fragments in op1: ${component}`)
  432. }
  433. append(result, component)
  434. }
  435. return result
  436. }
  437. if (typeof WEB !== 'undefined' && WEB !== null) {
  438. exports.types['text-tp2'] = type
  439. } else {
  440. module.exports = type
  441. }
  442. function __guard__(value, transform) {
  443. return typeof value !== 'undefined' && value !== null
  444. ? transform(value)
  445. : undefined
  446. }