text-composable.coffee 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261
  1. # An alternate composable implementation for text. This is much closer
  2. # to the implementation used by google wave.
  3. #
  4. # Ops are lists of components which iterate over the whole document.
  5. # Components are either:
  6. # A number N: Skip N characters in the original document
  7. # {i:'str'}: Insert 'str' at the current position in the document
  8. # {d:'str'}: Delete 'str', which appears at the current position in the document
  9. #
  10. # Eg: [3, {i:'hi'}, 5, {d:'internet'}]
  11. #
  12. # Snapshots are strings.
  13. p = -> #require('util').debug
  14. i = -> #require('util').inspect
  15. exports = if WEB? then {} else module.exports
  16. exports.name = 'text-composable'
  17. exports.create = -> ''
  18. # -------- Utility methods
  19. checkOp = (op) ->
  20. throw new Error('Op must be an array of components') unless Array.isArray(op)
  21. last = null
  22. for c in op
  23. if typeof(c) == 'object'
  24. throw new Error("Invalid op component: #{i c}") unless (c.i? && c.i.length > 0) or (c.d? && c.d.length > 0)
  25. else
  26. throw new Error('Op components must be objects or numbers') unless typeof(c) == 'number'
  27. throw new Error('Skip components must be a positive number') unless c > 0
  28. throw new Error('Adjacent skip components should be added') if typeof(last) == 'number'
  29. last = c
  30. # Makes a function for appending components to a given op.
  31. # Exported for the randomOpGenerator.
  32. exports._makeAppend = makeAppend = (op) -> (component) ->
  33. if component == 0 || component.i == '' || component.d == ''
  34. return
  35. else if op.length == 0
  36. op.push component
  37. else if typeof(component) == 'number' && typeof(op[op.length - 1]) == 'number'
  38. op[op.length - 1] += component
  39. else if component.i? && op[op.length - 1].i?
  40. op[op.length - 1].i += component.i
  41. else if component.d? && op[op.length - 1].d?
  42. op[op.length - 1].d += component.d
  43. else
  44. op.push component
  45. # checkOp op
  46. # Makes 2 functions for taking components from the start of an op, and for peeking
  47. # at the next op that could be taken.
  48. makeTake = (op) ->
  49. # The index of the next component to take
  50. idx = 0
  51. # The offset into the component
  52. offset = 0
  53. # Take up to length n from the front of op. If n is null, take the next
  54. # op component. If indivisableField == 'd', delete components won't be separated.
  55. # If indivisableField == 'i', insert components won't be separated.
  56. take = (n, indivisableField) ->
  57. return null if idx == op.length
  58. #assert.notStrictEqual op.length, i, 'The op is too short to traverse the document'
  59. if typeof(op[idx]) == 'number'
  60. if !n? or op[idx] - offset <= n
  61. c = op[idx] - offset
  62. ++idx; offset = 0
  63. c
  64. else
  65. offset += n
  66. n
  67. else
  68. # Take from the string
  69. field = if op[idx].i then 'i' else 'd'
  70. c = {}
  71. if !n? or op[idx][field].length - offset <= n or field == indivisableField
  72. c[field] = op[idx][field][offset..]
  73. ++idx; offset = 0
  74. else
  75. c[field] = op[idx][field][offset...(offset + n)]
  76. offset += n
  77. c
  78. peekType = () ->
  79. op[idx]
  80. [take, peekType]
  81. # Find and return the length of an op component
  82. componentLength = (component) ->
  83. if typeof(component) == 'number'
  84. component
  85. else if component.i?
  86. component.i.length
  87. else
  88. component.d.length
  89. # Normalize an op, removing all empty skips and empty inserts / deletes. Concatenate
  90. # adjacent inserts and deletes.
  91. exports.normalize = (op) ->
  92. newOp = []
  93. append = makeAppend newOp
  94. append component for component in op
  95. newOp
  96. # Apply the op to the string. Returns the new string.
  97. exports.apply = (str, op) ->
  98. p "Applying #{i op} to '#{str}'"
  99. throw new Error('Snapshot should be a string') unless typeof(str) == 'string'
  100. checkOp op
  101. pos = 0
  102. newDoc = []
  103. for component in op
  104. if typeof(component) == 'number'
  105. throw new Error('The op is too long for this document') if component > str.length
  106. newDoc.push str[...component]
  107. str = str[component..]
  108. else if component.i?
  109. newDoc.push component.i
  110. else
  111. throw new Error("The deleted text '#{component.d}' doesn't match the next characters in the document '#{str[...component.d.length]}'") unless component.d == str[...component.d.length]
  112. str = str[component.d.length..]
  113. throw new Error("The applied op doesn't traverse the entire document") unless '' == str
  114. newDoc.join ''
  115. # transform op1 by op2. Return transformed version of op1.
  116. # op1 and op2 are unchanged by transform.
  117. exports.transform = (op, otherOp, side) ->
  118. throw new Error "side (#{side} must be 'left' or 'right'" unless side == 'left' or side == 'right'
  119. checkOp op
  120. checkOp otherOp
  121. newOp = []
  122. append = makeAppend newOp
  123. [take, peek] = makeTake op
  124. for component in otherOp
  125. if typeof(component) == 'number' # Skip
  126. length = component
  127. while length > 0
  128. chunk = take(length, 'i')
  129. throw new Error('The op traverses more elements than the document has') unless chunk != null
  130. append chunk
  131. length -= componentLength chunk unless typeof(chunk) == 'object' && chunk.i?
  132. else if component.i? # Insert
  133. if side == 'left'
  134. # The left insert should go first.
  135. o = peek()
  136. append take() if o?.i
  137. # Otherwise, skip the inserted text.
  138. append(component.i.length)
  139. else # Delete.
  140. #assert.ok component.d
  141. length = component.d.length
  142. while length > 0
  143. chunk = take(length, 'i')
  144. throw new Error('The op traverses more elements than the document has') unless chunk != null
  145. if typeof(chunk) == 'number'
  146. length -= chunk
  147. else if chunk.i?
  148. append(chunk)
  149. else
  150. #assert.ok chunk.d
  151. # The delete is unnecessary now.
  152. length -= chunk.d.length
  153. # Append extras from op1
  154. while (component = take())
  155. throw new Error "Remaining fragments in the op: #{i component}" unless component?.i?
  156. append component
  157. newOp
  158. # Compose 2 ops into 1 op.
  159. exports.compose = (op1, op2) ->
  160. p "COMPOSE #{i op1} + #{i op2}"
  161. checkOp op1
  162. checkOp op2
  163. result = []
  164. append = makeAppend result
  165. [take, _] = makeTake op1
  166. for component in op2
  167. if typeof(component) == 'number' # Skip
  168. length = component
  169. while length > 0
  170. chunk = take(length, 'd')
  171. throw new Error('The op traverses more elements than the document has') unless chunk != null
  172. append chunk
  173. length -= componentLength chunk unless typeof(chunk) == 'object' && chunk.d?
  174. else if component.i? # Insert
  175. append {i:component.i}
  176. else # Delete
  177. offset = 0
  178. while offset < component.d.length
  179. chunk = take(component.d.length - offset, 'd')
  180. throw new Error('The op traverses more elements than the document has') unless chunk != null
  181. # If its delete, append it. If its skip, drop it and decrease length. If its insert, check the strings match, drop it and decrease length.
  182. if typeof(chunk) == 'number'
  183. append {d:component.d[offset...(offset + chunk)]}
  184. offset += chunk
  185. else if chunk.i?
  186. throw new Error("The deleted text doesn't match the inserted text") unless component.d[offset...(offset + chunk.i.length)] == chunk.i
  187. offset += chunk.i.length
  188. # The ops cancel each other out.
  189. else
  190. # Delete
  191. append chunk
  192. # Append extras from op1
  193. while (component = take())
  194. throw new Error "Trailing stuff in op1 #{i component}" unless component?.d?
  195. append component
  196. result
  197. invertComponent = (c) ->
  198. if typeof(c) == 'number'
  199. c
  200. else if c.i?
  201. {d:c.i}
  202. else
  203. {i:c.d}
  204. # Invert an op
  205. exports.invert = (op) ->
  206. result = []
  207. append = makeAppend result
  208. append(invertComponent component) for component in op
  209. result
  210. if window?
  211. window.ot ||= {}
  212. window.ot.types ||= {}
  213. window.ot.types.text = exports