Initial Commit -> Text collaboration works
This commit is contained in:
284
lib/Types/BasicTypes.coffee
Normal file
284
lib/Types/BasicTypes.coffee
Normal file
@@ -0,0 +1,284 @@
|
||||
module.exports = (HB)->
|
||||
# @see Engine.parse
|
||||
parser = {}
|
||||
execution_listener = []
|
||||
|
||||
#
|
||||
# A generic interface to operations.
|
||||
#
|
||||
class Operation
|
||||
# @param {Object} uid A unique identifier
|
||||
# @see HistoryBuffer.getNextOperationIdentifier
|
||||
constructor: ({'creator': @creator, 'op_number' : @op_number})->
|
||||
|
||||
# Computes a unique identifier (uid).
|
||||
getUid: ()->
|
||||
{ 'creator': @creator, 'op_number': @op_number }
|
||||
|
||||
execute: ()->
|
||||
for l in execution_listener
|
||||
l @toJson()
|
||||
@
|
||||
|
||||
#
|
||||
# Operations may depend on other operations (linked lists, etc.). The saveOperation and validateSavedOperations methods provide
|
||||
# an easy way to refer to these operations via an uid or object reference.
|
||||
#
|
||||
# For example: We can create a new Delete operation that deletes the operation $o like this
|
||||
# - var d = new Delete(uid, $o); or
|
||||
# - var d = new Delete(uid, $o.getUid());
|
||||
# Either way we want to access $o via d.deletes. This is possible after calling validateSavedOperations.
|
||||
#
|
||||
# @overload saveOperation(name, op_uid)
|
||||
# @param {String} name The name of the operation. After validating (with validateSavedOperations) the instantiated operation will be accessible via this[name].
|
||||
# @param {Object} op_uid A uid that refers to an operation
|
||||
# @overload saveOperation(name, op)
|
||||
# @param {String} name The name of the operation. After calling this function op is accessible via this[name].
|
||||
# @param {Operation} op An Operation object
|
||||
#
|
||||
saveOperation: (name, op)->
|
||||
# Every instance of $Operation must have an $execute function.
|
||||
# We use duck-typing to check if op is instantiated since there
|
||||
# could exist multiple classes of $Operation
|
||||
if op?.execute?
|
||||
# is instantiated
|
||||
@[name] = op
|
||||
else if op?
|
||||
# not initialized. Do it when calling $validateSavedOperations()
|
||||
@unchecked ?= {}
|
||||
@unchecked[name] = op
|
||||
|
||||
#
|
||||
# After calling this function all not instantiated operations will be accessible.
|
||||
# @see Operation.saveOperation
|
||||
#
|
||||
# @return [Boolean] Whether it was possible to instantiate all operations.
|
||||
#
|
||||
validateSavedOperations: ()->
|
||||
uninstantiated = {}
|
||||
success = @
|
||||
for name, op_uid of @unchecked
|
||||
op = HB.getOperation op_uid
|
||||
if op
|
||||
@[name] = op
|
||||
else
|
||||
uninstantiated[name] = op
|
||||
success = false
|
||||
delete @unchecked
|
||||
if not success
|
||||
@unchecked = uninstantiated
|
||||
success
|
||||
|
||||
|
||||
|
||||
#
|
||||
# A simple delete-type operation.
|
||||
#
|
||||
class Delete extends Operation
|
||||
constructor: (uid, deletes)->
|
||||
@saveOperation 'deletes', deletes
|
||||
super uid
|
||||
|
||||
#
|
||||
# Convert all relevant information of this operation to the json-format.
|
||||
# This result can be sent to other clients.
|
||||
#
|
||||
toJson: ()->
|
||||
{
|
||||
'type': "Delete"
|
||||
'uid': @getUid()
|
||||
'deletes': @deletes.getUid()
|
||||
}
|
||||
|
||||
execute: ()->
|
||||
if @validateSavedOperations()
|
||||
@deletes.applyDelete @
|
||||
super
|
||||
@
|
||||
else
|
||||
false
|
||||
|
||||
#
|
||||
# Define how to parse $Delete operations.
|
||||
#
|
||||
parser['Delete'] = ({'uid' : uid, 'deletes': deletes_uid})->
|
||||
new D uid, deletes_uid
|
||||
|
||||
#
|
||||
# A simple insert-type operation.
|
||||
#
|
||||
# An insert operation is always positioned between two other insert operations.
|
||||
# Internally this is realized as associative lists, whereby each insert operation has a predecessor and a successor.
|
||||
# For the sake of efficiency we maintain two lists:
|
||||
# - The short-list (abbrev. sl) maintains only the operations that are not deleted
|
||||
# - The complete-list (abbrev. cl) maintains all operations
|
||||
#
|
||||
class Insert extends Operation
|
||||
# @param {Value} content The value of the insert operation. E.g. for strings content is a char.
|
||||
# @param {Object} creator A unique user identifier
|
||||
# @param {Integer} op_number This Number was assigned via getNextOperationIdentifier().
|
||||
# @param {Operation} prev_cl The predecessor of this operation in the complete-list (cl)
|
||||
# @param {Operation} next_cl The successor of this operation in the complete-list (cl)
|
||||
#
|
||||
# @see HistoryBuffer.getNextOperationIdentifier
|
||||
constructor: (uid, prev_cl, next_cl, origin)->
|
||||
@saveOperation 'prev_cl', prev_cl
|
||||
@saveOperation 'next_cl', next_cl
|
||||
if origin?
|
||||
@saveOperation 'origin', origin
|
||||
else
|
||||
@saveOperation 'origin', prev_cl
|
||||
super uid
|
||||
|
||||
applyDelete: (o)->
|
||||
@deleted_by ?= []
|
||||
@deleted_by.push o
|
||||
|
||||
#
|
||||
# If isDeleted() is true this operation won't be maintained in the sl
|
||||
#
|
||||
isDeleted: ()->
|
||||
@deleted_by?.length > 0
|
||||
|
||||
#
|
||||
# The amount of positions that $this operation was moved to the right.
|
||||
#
|
||||
getDistanceToOrigin: ()->
|
||||
d = 0
|
||||
o = @prev_cl
|
||||
while true
|
||||
if @origin is o
|
||||
break
|
||||
d++
|
||||
o = o.prev_cl
|
||||
d
|
||||
|
||||
#
|
||||
# Update the short list
|
||||
# TODO (Unused)
|
||||
update_sl: ()->
|
||||
o = @prev_cl
|
||||
update: (dest_cl,dest_sl)->
|
||||
while true
|
||||
if o.isDeleted()
|
||||
o = o[dest_cl]
|
||||
else
|
||||
@[dest_sl] = o
|
||||
|
||||
break
|
||||
update "prev_cl", "prev_sl"
|
||||
update "next_cl", "prev_sl"
|
||||
|
||||
|
||||
|
||||
#
|
||||
# Include this operation in the associative lists.
|
||||
#
|
||||
execute: ()->
|
||||
if not @validateSavedOperations()
|
||||
return false
|
||||
else
|
||||
if @prev_cl? and @next_cl?
|
||||
distance_to_origin = 0
|
||||
o = @prev_cl.next_cl
|
||||
i = 0
|
||||
# $this has to find a unique position between origin and the next known character
|
||||
# case 1: $origin equals $o.origin: the $creator parameter decides if left or right
|
||||
# let $OL= [o1,o2,o3,o4], whereby $this is to be inserted between o1 and o4
|
||||
# o2,o3 and o4 origin is 1 (the position of o2)
|
||||
# there is the case that $this.creator < o2.creator, but o3.creator < $this.creator
|
||||
# then o2 knows o3. Since on another client $OL could be [o1,o3,o4] the problem is complex
|
||||
# therefore $this would be always to the right of o3
|
||||
# case 2: $origin < $o.origin
|
||||
# if current $this insert_position > $o origin: $this ins
|
||||
# else $insert_position will not change (maybe we encounter case 1 later, then this will be to the right of $o)
|
||||
# case 3: $origin > $o.origin
|
||||
# $this insert_position is to the left of $o (forever!)
|
||||
while true
|
||||
if not o?
|
||||
# TODO: Debugging
|
||||
console.log JSON.stringify @prev_cl.getUid()
|
||||
console.log JSON.stringify @next_cl.getUid()
|
||||
if o isnt @next_cl
|
||||
# $o happened concurrently
|
||||
if o.getDistanceToOrigin() is i
|
||||
# case 1
|
||||
if o.creator < @creator
|
||||
@prev_cl = o
|
||||
distance_to_origin = i + 1
|
||||
else
|
||||
# nop
|
||||
else if o.getDistanceToOrigin() < i
|
||||
# case 2
|
||||
if i - distance_to_origin <= o.getDistanceToOrigin()
|
||||
@prev_cl = o
|
||||
distance_to_origin = i + 1
|
||||
else
|
||||
#nop
|
||||
else
|
||||
# case 3
|
||||
break
|
||||
i++
|
||||
o = o.next_cl
|
||||
else
|
||||
# $this knows that $o exists,
|
||||
break
|
||||
# now reconnect everything
|
||||
@next_cl = @prev_cl.next_cl
|
||||
@prev_cl.next_cl = @
|
||||
@next_cl.prev_cl = @
|
||||
super # notify the execution_listeners
|
||||
@
|
||||
|
||||
val: ()->
|
||||
throw new Error "Implement this function!"
|
||||
|
||||
#
|
||||
# A delimiter is placed at the end and at the beginning of the associative lists.
|
||||
# This is necessary in order to have a beginning and an end even if the content
|
||||
# of the Engine is empty.
|
||||
#
|
||||
class Delimiter extends Insert
|
||||
|
||||
isDeleted: ()->
|
||||
false
|
||||
|
||||
getDistanceToOrigin: ()->
|
||||
0
|
||||
|
||||
execute: ()->
|
||||
a = @validateSavedOperations()
|
||||
for l in execution_listener
|
||||
l @toJson()
|
||||
a
|
||||
|
||||
toJson: ()->
|
||||
{
|
||||
'type' : "Delimiter"
|
||||
'uid' : @getUid()
|
||||
'prev' : @prev_cl.getUid()
|
||||
'next' : @next_cl.getUid()
|
||||
}
|
||||
|
||||
parser['Delimiter'] = (json)->
|
||||
{
|
||||
'uid' : uid
|
||||
'prev' : prev
|
||||
'next' : next
|
||||
} = json
|
||||
new Delimiter uid, prev, next
|
||||
|
||||
# This is what this module exports after initializing it with the HistoryBuffer
|
||||
{
|
||||
'types' :
|
||||
'Delete' : Delete
|
||||
'Insert' : Insert
|
||||
'Delimiter': Delimiter
|
||||
'Operation': Operation
|
||||
'parser' : parser
|
||||
'execution_listener' : execution_listener
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
175
lib/Types/StructuredTypes.coffee
Normal file
175
lib/Types/StructuredTypes.coffee
Normal file
@@ -0,0 +1,175 @@
|
||||
_ = require "underscore"
|
||||
basic_types_uninitialized = require "./BasicTypes.coffee"
|
||||
|
||||
module.exports = (HB)->
|
||||
basic_types = basic_types_uninitialized HB
|
||||
types = basic_types.types
|
||||
parser = basic_types.parser
|
||||
|
||||
class MapManager
|
||||
constructor: ()->
|
||||
@map = {}
|
||||
|
||||
set: (name, content)->
|
||||
if not @map[name]?
|
||||
@map[name] = new Replaceable HB,
|
||||
@map[name].replace content
|
||||
|
||||
class ListManager extends types.Insert
|
||||
constructor: (uid, beginning, end, prev, next, origin)->
|
||||
if beginning? and end?
|
||||
saveOperation "beginning", beginning
|
||||
saveOperation "end", end
|
||||
else
|
||||
@beginning = HB.addOperation new types.Delimiter HB.getNextOperationIdentifier(), undefined, undefined
|
||||
@end = HB.addOperation new types.Delimiter HB.getNextOperationIdentifier(), @beginning, undefined
|
||||
@beginning.next_cl = @end
|
||||
super uid, prev, next, origin
|
||||
|
||||
# Get the element previous to the delemiter at the end
|
||||
getLastOperation: ()->
|
||||
@end.prev_cl
|
||||
|
||||
# similar to the above
|
||||
getFirstOperation: ()->
|
||||
@beginning.next_cl
|
||||
|
||||
# Transforms the the list to an array
|
||||
# Doesn't return left-right delimiter.
|
||||
toArray: ()->
|
||||
o = @beginning.next_cl
|
||||
result = []
|
||||
while o isnt @end
|
||||
result.push o
|
||||
o = o.next_cl
|
||||
result
|
||||
|
||||
#
|
||||
# Retrieves the x-th not deleted element.
|
||||
#
|
||||
getOperationByPosition: (position)->
|
||||
o = @beginning.next_cl
|
||||
if position > 0
|
||||
while true
|
||||
o = o.next_cl
|
||||
if not o.isDeleted()
|
||||
position -= 1
|
||||
if position is 0
|
||||
break
|
||||
if o instanceof types.Delimiter
|
||||
throw new Error "position parameter exceeded the length of the document!"
|
||||
o
|
||||
|
||||
|
||||
class ReplaceManager extends ListManager
|
||||
constructor: (initial_content, uid, beginning, end, prev, next, origin)->
|
||||
super uid, beginning, end, prev, next, origin
|
||||
if initial_content?
|
||||
@replace initial_content
|
||||
|
||||
replace: (content)->
|
||||
o = @getLastOperation()
|
||||
op = new Replaceable content, @, HB.getNextOperationIdentifier(), o, o.next_cl
|
||||
HB.addOperation(op).execute()
|
||||
|
||||
val: ()->
|
||||
o = @getLastOperation()
|
||||
if o instanceof types.Delimiter
|
||||
throw new Error "dtrn"
|
||||
o.val()
|
||||
|
||||
toJson: ()->
|
||||
json =
|
||||
{
|
||||
'type': "ReplaceManager"
|
||||
'uid' : @getUid()
|
||||
'beginning' : @beginning
|
||||
'end' : @end
|
||||
}
|
||||
if @prev_cl? and @next_cl?
|
||||
json['prev'] = @prev_cl.getUid()
|
||||
json['next'] = @next_cl.getUid()
|
||||
if @origin? and @origin isnt @prev_cl
|
||||
json["origin"] = @origin.getUid()
|
||||
json
|
||||
|
||||
parser["ReplaceManager"] = (json)->
|
||||
{
|
||||
'content' : content
|
||||
'uid' : uid
|
||||
'prev': prev
|
||||
'next': next
|
||||
'origin' : origin
|
||||
'beginning' : beginning
|
||||
'end' : end
|
||||
} = json
|
||||
new ReplaceManager content, uid, beginning, end, prev, next, origin
|
||||
|
||||
|
||||
#
|
||||
# Extends the basic Insert type.
|
||||
#
|
||||
class Replaceable extends types.Insert
|
||||
constructor: (content, parent, uid, prev, next, origin)->
|
||||
@saveOperation 'content', content
|
||||
@saveOperation 'parent', parent
|
||||
if not (prev? and next?)
|
||||
throw new Error "You must define prev, and next for Replaceable-types!"
|
||||
super uid, prev, next, origin
|
||||
|
||||
#
|
||||
#
|
||||
val: ()->
|
||||
@content
|
||||
|
||||
replace: (content)->
|
||||
@parent.replace content
|
||||
|
||||
execute: ()->
|
||||
super
|
||||
@content.setReplaceManager?(@parent)
|
||||
@
|
||||
|
||||
#
|
||||
# Convert all relevant information of this operation to the json-format.
|
||||
# This result can be send to other clients.
|
||||
#
|
||||
toJson: ()->
|
||||
json =
|
||||
{
|
||||
'type': "Replaceable"
|
||||
'content': @content.getUid()
|
||||
'ReplaceManager' : @parent
|
||||
'prev': @prev_cl.getUid()
|
||||
'next': @next_cl.getUid()
|
||||
'uid' : @getUid()
|
||||
}
|
||||
if @origin? and @origin isnt @prev_cl
|
||||
json["origin"] = @origin.getUid()
|
||||
json
|
||||
|
||||
parser["Replaceable"] = (json)->
|
||||
{
|
||||
'content' : content
|
||||
'ReplaceManager' : parent
|
||||
'uid' : uid
|
||||
'prev': prev
|
||||
'next': next
|
||||
'origin' : origin
|
||||
} = json
|
||||
new Replaceable content, parent, uid, prev, next, origin
|
||||
|
||||
|
||||
|
||||
types['ListManager'] = ListManager
|
||||
types['MapManager'] = MapManager
|
||||
types['ReplaceManager'] = ReplaceManager
|
||||
types['Replaceable'] = Replaceable
|
||||
|
||||
basic_types
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
145
lib/Types/TextTypes.coffee
Normal file
145
lib/Types/TextTypes.coffee
Normal file
@@ -0,0 +1,145 @@
|
||||
_ = require "underscore"
|
||||
structured_types_uninitialized = require "./StructuredTypes.coffee"
|
||||
|
||||
module.exports = (HB)->
|
||||
structured_types = structured_types_uninitialized HB
|
||||
types = structured_types.types
|
||||
parser = structured_types.parser
|
||||
|
||||
#
|
||||
# At the moment TextDelete type equals the Delete type in BasicTypes.
|
||||
# @see BasicTypes.Delete
|
||||
#
|
||||
class TextDelete extends types.Delete
|
||||
parser["TextDelete"] = parser["Delete"]
|
||||
|
||||
#
|
||||
# Extends the basic Insert type.
|
||||
#
|
||||
class TextInsert extends types.Insert
|
||||
constructor: (@content, uid, prev, next, origin)->
|
||||
if not (prev? and next?)
|
||||
throw new Error "You must define prev, and next for TextInsert-types!"
|
||||
super uid, prev, next, origin
|
||||
#
|
||||
# Retrieve the effective length of the $content of this operation.
|
||||
#
|
||||
getLength: ()->
|
||||
if @isDeleted()
|
||||
0
|
||||
else
|
||||
@content.length
|
||||
|
||||
#
|
||||
# The result will be concatenated with the results from the other insert operations
|
||||
# in order to retrieve the content of the engine.
|
||||
# @see HistoryBuffer.toExecutedArray
|
||||
#
|
||||
val: (current_position)->
|
||||
if @isDeleted()
|
||||
""
|
||||
else
|
||||
@content
|
||||
|
||||
#
|
||||
# Convert all relevant information of this operation to the json-format.
|
||||
# This result can be send to other clients.
|
||||
#
|
||||
toJson: ()->
|
||||
json =
|
||||
{
|
||||
'type': "TextInsert"
|
||||
'content': @content
|
||||
'uid' : @getUid()
|
||||
'prev': @prev_cl.getUid()
|
||||
'next': @next_cl.getUid()
|
||||
}
|
||||
if @origin? and @origin isnt @prev_cl
|
||||
json["origin"] = @origin.getUid()
|
||||
json
|
||||
|
||||
parser["TextInsert"] = (json)->
|
||||
{
|
||||
'content' : content
|
||||
'uid' : uid
|
||||
'prev': prev
|
||||
'next': next
|
||||
'origin' : origin
|
||||
} = json
|
||||
new TextInsert content, uid, prev, next, origin
|
||||
|
||||
class Word extends types.ListManager
|
||||
constructor: (uid, prev, next, origin)->
|
||||
super uid, prev, next, origin
|
||||
|
||||
# inserts a
|
||||
insertText: (position, content)->
|
||||
o = @getOperationByPosition position
|
||||
for c in content
|
||||
op = new TextInsert c, HB.getNextOperationIdentifier(), o.prev_cl, o
|
||||
HB.addOperation(op).execute()
|
||||
|
||||
# Creates a set of delete operations
|
||||
deleteText: (position, length)->
|
||||
o = @getOperationByPosition position
|
||||
|
||||
for i in [0...length]
|
||||
d = HB.addOperation(new TextDelete HB.getNextOperationIdentifier(), o).execute()
|
||||
o = o.next_cl
|
||||
while o.isDeleted()
|
||||
if o instanceof types.Delimiter
|
||||
throw new Error "You can't delete more than there is.."
|
||||
o = o.next_cl
|
||||
d.toJson()
|
||||
|
||||
replaceText: (text)->
|
||||
if @replace_manager?
|
||||
word = HB.addOperation(new Word HB.getNextOperationIdentifier()).execute()
|
||||
word.insertText 0, text
|
||||
@replace_manager.replace(word)
|
||||
else
|
||||
throw new Error "This type is currently not maintained by a ReplaceManager!"
|
||||
|
||||
val: ()->
|
||||
c = for o in @toArray()
|
||||
if o.val?
|
||||
o.val()
|
||||
else
|
||||
""
|
||||
c.join('')
|
||||
|
||||
setReplaceManager: (op)->
|
||||
@saveOperation 'replace_manager', op
|
||||
@validateSavedOperations
|
||||
|
||||
toJson: ()->
|
||||
json = {
|
||||
'type': "TextInsert"
|
||||
'content': @content
|
||||
'uid' : @getUid()
|
||||
}
|
||||
if @prev_cl?
|
||||
json['prev'] = @prev_cl.getUid()
|
||||
if @next_cl?
|
||||
json['next'] = @next_cl.getUid()
|
||||
if @origin? and @origin isnt @prev_cl
|
||||
json["origin"] = @origin.getUid()
|
||||
json
|
||||
|
||||
parser['Word'] = (json)->
|
||||
{
|
||||
'content' : content
|
||||
'uid' : uid
|
||||
'prev': prev
|
||||
'next': next
|
||||
'origin' : origin
|
||||
} = json
|
||||
new Word uid, prev, next, origin
|
||||
|
||||
types['TextInsert'] = TextInsert
|
||||
types['TextDelete'] = TextDelete
|
||||
types['Word'] = Word
|
||||
|
||||
structured_types
|
||||
|
||||
|
||||
1
lib/Types/XmlTypes.coffee
Normal file
1
lib/Types/XmlTypes.coffee
Normal file
@@ -0,0 +1 @@
|
||||
_ = require "underscore"
|
||||
Reference in New Issue
Block a user