text.coffee 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209
  1. # A simple text implementation
  2. #
  3. # Operations are lists of components.
  4. # Each component either inserts or deletes at a specified position in the document.
  5. #
  6. # Components are either:
  7. # {i:'str', p:100}: Insert 'str' at position 100 in the document
  8. # {d:'str', p:100}: Delete 'str' at position 100 in the document
  9. #
  10. # Components in an operation are executed sequentially, so the position of components
  11. # assumes previous components have already executed.
  12. #
  13. # Eg: This op:
  14. # [{i:'abc', p:0}]
  15. # is equivalent to this op:
  16. # [{i:'a', p:0}, {i:'b', p:1}, {i:'c', p:2}]
  17. # NOTE: The global scope here is shared with other sharejs files when built with closure.
  18. # Be careful what ends up in your namespace.
  19. text = {}
  20. text.name = 'text'
  21. text.create = -> ''
  22. strInject = (s1, pos, s2) -> s1[...pos] + s2 + s1[pos..]
  23. checkValidComponent = (c) ->
  24. throw new Error 'component missing position field' if typeof c.p != 'number'
  25. i_type = typeof c.i
  26. d_type = typeof c.d
  27. throw new Error 'component needs an i or d field' unless (i_type == 'string') ^ (d_type == 'string')
  28. throw new Error 'position cannot be negative' unless c.p >= 0
  29. checkValidOp = (op) ->
  30. checkValidComponent(c) for c in op
  31. true
  32. text.apply = (snapshot, op) ->
  33. checkValidOp op
  34. for component in op
  35. if component.i?
  36. snapshot = strInject snapshot, component.p, component.i
  37. else
  38. deleted = snapshot[component.p...(component.p + component.d.length)]
  39. throw new Error "Delete component '#{component.d}' does not match deleted text '#{deleted}'" unless component.d == deleted
  40. snapshot = snapshot[...component.p] + snapshot[(component.p + component.d.length)..]
  41. snapshot
  42. # Exported for use by the random op generator.
  43. #
  44. # For simplicity, this version of append does not compress adjacent inserts and deletes of
  45. # the same text. It would be nice to change that at some stage.
  46. text._append = append = (newOp, c) ->
  47. return if c.i == '' or c.d == ''
  48. if newOp.length == 0
  49. newOp.push c
  50. else
  51. last = newOp[newOp.length - 1]
  52. # Compose the insert into the previous insert if possible
  53. if last.i? && c.i? and last.p <= c.p <= (last.p + last.i.length)
  54. newOp[newOp.length - 1] = {i:strInject(last.i, c.p - last.p, c.i), p:last.p}
  55. else if last.d? && c.d? and c.p <= last.p <= (c.p + c.d.length)
  56. newOp[newOp.length - 1] = {d:strInject(c.d, last.p - c.p, last.d), p:c.p}
  57. else
  58. newOp.push c
  59. text.compose = (op1, op2) ->
  60. checkValidOp op1
  61. checkValidOp op2
  62. newOp = op1.slice()
  63. append newOp, c for c in op2
  64. newOp
  65. # Attempt to compress the op components together 'as much as possible'.
  66. # This implementation preserves order and preserves create/delete pairs.
  67. text.compress = (op) -> text.compose [], op
  68. text.normalize = (op) ->
  69. newOp = []
  70. # Normalize should allow ops which are a single (unwrapped) component:
  71. # {i:'asdf', p:23}.
  72. # There's no good way to test if something is an array:
  73. # http://perfectionkills.com/instanceof-considered-harmful-or-how-to-write-a-robust-isarray/
  74. # so this is probably the least bad solution.
  75. op = [op] if op.i? or op.p?
  76. for c in op
  77. c.p ?= 0
  78. append newOp, c
  79. newOp
  80. # This helper method transforms a position by an op component.
  81. #
  82. # If c is an insert, insertAfter specifies whether the transform
  83. # is pushed after the insert (true) or before it (false).
  84. #
  85. # insertAfter is optional for deletes.
  86. transformPosition = (pos, c, insertAfter) ->
  87. if c.i?
  88. if c.p < pos || (c.p == pos && insertAfter)
  89. pos + c.i.length
  90. else
  91. pos
  92. else
  93. # I think this could also be written as: Math.min(c.p, Math.min(c.p - otherC.p, otherC.d.length))
  94. # but I think its harder to read that way, and it compiles using ternary operators anyway
  95. # so its no slower written like this.
  96. if pos <= c.p
  97. pos
  98. else if pos <= c.p + c.d.length
  99. c.p
  100. else
  101. pos - c.d.length
  102. # Helper method to transform a cursor position as a result of an op.
  103. #
  104. # Like transformPosition above, if c is an insert, insertAfter specifies whether the cursor position
  105. # is pushed after an insert (true) or before it (false).
  106. text.transformCursor = (position, op, side) ->
  107. insertAfter = side == 'right'
  108. position = transformPosition position, c, insertAfter for c in op
  109. position
  110. # Transform an op component by another op component. Asymmetric.
  111. # The result will be appended to destination.
  112. #
  113. # exported for use in JSON type
  114. text._tc = transformComponent = (dest, c, otherC, side) ->
  115. checkValidOp [c]
  116. checkValidOp [otherC]
  117. if c.i?
  118. append dest, {i:c.i, p:transformPosition(c.p, otherC, side == 'right')}
  119. else # Delete
  120. if otherC.i? # delete vs insert
  121. s = c.d
  122. if c.p < otherC.p
  123. append dest, {d:s[...otherC.p - c.p], p:c.p}
  124. s = s[(otherC.p - c.p)..]
  125. if s != ''
  126. append dest, {d:s, p:c.p + otherC.i.length}
  127. else # Delete vs delete
  128. if c.p >= otherC.p + otherC.d.length
  129. append dest, {d:c.d, p:c.p - otherC.d.length}
  130. else if c.p + c.d.length <= otherC.p
  131. append dest, c
  132. else
  133. # They overlap somewhere.
  134. newC = {d:'', p:c.p}
  135. if c.p < otherC.p
  136. newC.d = c.d[...(otherC.p - c.p)]
  137. if c.p + c.d.length > otherC.p + otherC.d.length
  138. newC.d += c.d[(otherC.p + otherC.d.length - c.p)..]
  139. # This is entirely optional - just for a check that the deleted
  140. # text in the two ops matches
  141. intersectStart = Math.max c.p, otherC.p
  142. intersectEnd = Math.min c.p + c.d.length, otherC.p + otherC.d.length
  143. cIntersect = c.d[intersectStart - c.p...intersectEnd - c.p]
  144. otherIntersect = otherC.d[intersectStart - otherC.p...intersectEnd - otherC.p]
  145. throw new Error 'Delete ops delete different text in the same region of the document' unless cIntersect == otherIntersect
  146. if newC.d != ''
  147. # This could be rewritten similarly to insert v delete, above.
  148. newC.p = transformPosition newC.p, otherC
  149. append dest, newC
  150. dest
  151. invertComponent = (c) ->
  152. if c.i?
  153. {d:c.i, p:c.p}
  154. else
  155. {i:c.d, p:c.p}
  156. # No need to use append for invert, because the components won't be able to
  157. # cancel with one another.
  158. text.invert = (op) -> (invertComponent c for c in op.slice().reverse())
  159. if WEB?
  160. exports.types ||= {}
  161. # This is kind of awful - come up with a better way to hook this helper code up.
  162. bootstrapTransform(text, transformComponent, checkValidOp, append)
  163. # [] is used to prevent closure from renaming types.text
  164. exports.types.text = text
  165. else
  166. module.exports = text
  167. # The text type really shouldn't need this - it should be possible to define
  168. # an efficient transform function by making a sort of transform map and passing each
  169. # op component through it.
  170. require('./helpers').bootstrapTransform(text, transformComponent, checkValidOp, append)