Initial Commit -> Text collaboration works
This commit is contained in:
51
lib/Connectors/TestConnector.coffee
Normal file
51
lib/Connectors/TestConnector.coffee
Normal file
@@ -0,0 +1,51 @@
|
||||
|
||||
_ = require "underscore"
|
||||
|
||||
module.exports = (user_list)->
|
||||
class TestConnector
|
||||
constructor: (@engine, @HB, @execution_listener)->
|
||||
send_ = (o)=>
|
||||
@send o
|
||||
@execution_listener.push send_
|
||||
|
||||
if not (user_list?.length is 0)
|
||||
@engine.applyOps user_list[0].getHistoryBuffer().toJson()
|
||||
|
||||
@unexecuted = {}
|
||||
@applied_operations = []
|
||||
|
||||
appliedOperationsListener = (o)=>
|
||||
@applied_operations.push o
|
||||
@execution_listener.push appliedOperationsListener
|
||||
|
||||
getOpsInExecutionOrder: ()->
|
||||
@applied_operations
|
||||
|
||||
getRootElement: ()->
|
||||
if user_list.length > 0
|
||||
user_list[0].getRootElement()
|
||||
|
||||
send: (o)->
|
||||
if o.creator is @HB.getUserId()
|
||||
for user in user_list
|
||||
if not user.getUserId() is @HB.getUserId()
|
||||
user.getConnector().receive(o)
|
||||
|
||||
receive: (o)->
|
||||
@unexecuted[o.creator] ?= []
|
||||
@unexecuted[o.creator].push o
|
||||
|
||||
flushOne: (user)->
|
||||
if @unexecuted[user]?.length > 0
|
||||
@engine.applyOp @unexecuted[user].shift()
|
||||
|
||||
flushOneRandom: ()->
|
||||
@flushOne (_.random 0, (user_list.length-1))
|
||||
|
||||
flushAll: ()->
|
||||
for ops of @unexecuted
|
||||
@engine.applyOps ops
|
||||
@unexecuted = {}
|
||||
|
||||
sync: ()->
|
||||
throw new Error "Can't use this a.t.m."
|
||||
44
lib/Engine.coffee
Normal file
44
lib/Engine.coffee
Normal file
@@ -0,0 +1,44 @@
|
||||
_ = require "underscore"
|
||||
|
||||
class Engine
|
||||
constructor: (@HB, @parser)->
|
||||
@unprocessed_ops = []
|
||||
|
||||
parseOperation: (json)->
|
||||
typeParser = @parser[json.type]
|
||||
if typeParser?
|
||||
typeParser json
|
||||
else
|
||||
throw new Error "You forgot to specify a parser for type #{json.type}. The message is #{JSON.stringify json}."
|
||||
|
||||
# TODO:
|
||||
applyOps: (ops)->
|
||||
for o in ops
|
||||
@applyOp o
|
||||
|
||||
applyOp: (op_json)->
|
||||
# $parse_and_execute will return false if $o_json was parsed and executed, otherwise the parsed operadion
|
||||
o = @parseOperation o_json
|
||||
@HB.addOperation o
|
||||
if not o.execute()
|
||||
@unprocessed_ops.push o
|
||||
unprocessed = []
|
||||
for op in @unprocessed_ops
|
||||
if not op.execute()
|
||||
unprocessed.push op
|
||||
@unprocessed_ops = unprocessed
|
||||
|
||||
|
||||
module.exports = Engine
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
47
lib/Frameworks/TextYatta.coffee
Normal file
47
lib/Frameworks/TextYatta.coffee
Normal file
@@ -0,0 +1,47 @@
|
||||
|
||||
text_types_uninitialized = require "../Types/TextTypes.coffee"
|
||||
HistoryBuffer = require "../HistoryBuffer.coffee"
|
||||
Engine = require "../Engine.coffee"
|
||||
|
||||
class TextYatta
|
||||
constructor: (user_id, Connector)->
|
||||
@HB = new HistoryBuffer user_id
|
||||
text_types = text_types_uninitialized @HB
|
||||
@engine = new Engine @HB, text_types.parser
|
||||
@connector = new Connector @engine, @HB, text_types.execution_listener
|
||||
@root_element = @connector.getRootElement()
|
||||
if not @root_element?
|
||||
first_word = new text_types.types.Word @HB.getNextOperationIdentifier()
|
||||
@HB.addOperation(first_word)
|
||||
first_word.execute()
|
||||
@root_element = @HB.addOperation(new text_types.types.ReplaceManager first_word, @HB.getNextOperationIdentifier()).execute()
|
||||
|
||||
getRootElement: ()->
|
||||
@root_element
|
||||
|
||||
getEngine: ()->
|
||||
@engine
|
||||
|
||||
getConnector: ()->
|
||||
@connector
|
||||
|
||||
getHistoryBuffer: ()->
|
||||
@HB
|
||||
|
||||
getUserId: ()->
|
||||
@HB.getUserId()
|
||||
|
||||
val: ()->
|
||||
@root_element.val().val()
|
||||
|
||||
insertText: (pos, content)->
|
||||
@root_element.val().insertText pos, content
|
||||
|
||||
deleteText: (pos, length)->
|
||||
@root_element.val().deleteText pos, length
|
||||
|
||||
replaceText: (text)->
|
||||
@root_element.val().replaceText text
|
||||
|
||||
|
||||
module.exports = TextYatta
|
||||
75
lib/HistoryBuffer.coffee
Normal file
75
lib/HistoryBuffer.coffee
Normal file
@@ -0,0 +1,75 @@
|
||||
_ = require "underscore"
|
||||
|
||||
#
|
||||
# An object that holds all applied operations.
|
||||
#
|
||||
# @note The HistoryBuffer is commonly abbreviated to HB.
|
||||
#
|
||||
class HistoryBuffer
|
||||
# @overload new HistoryBuffer()
|
||||
# Creates an empty HB.
|
||||
# @param {Object} user_id Creator of the HB.
|
||||
# @overload new HistoryBuffer(initial_content)
|
||||
# Creates an HB with initial operations that represent the initial_value.
|
||||
# @param {Array<Object>} initial_content Initial content of the DUC
|
||||
# @see DUC DUC - Document Under Collaboration
|
||||
constructor: (@user_id)->
|
||||
@operation_counter = {}
|
||||
@buffer = {}
|
||||
@change_listeners = []
|
||||
|
||||
getUserId: ()->
|
||||
@user_id
|
||||
|
||||
getOperationCounter: ()->
|
||||
_.clone @operation_counter
|
||||
|
||||
toJson: ()->
|
||||
json = []
|
||||
for user in @buffer
|
||||
for o of user
|
||||
json.push o.toJson()
|
||||
json
|
||||
|
||||
# Get the number of operations that were created by a user.
|
||||
# Accordingly you will get the next operation number that is expected from that user.
|
||||
# You'll get new results only if you added the operation with $addOperation.
|
||||
#
|
||||
getNextOperationIdentifier: (user_id)->
|
||||
if not user_id?
|
||||
user_id = @user_id
|
||||
if not @operation_counter[user_id]?
|
||||
@operation_counter[user_id] = 0
|
||||
uid = {
|
||||
'creator' : user_id
|
||||
'op_number' : @operation_counter[user_id]
|
||||
}
|
||||
@operation_counter[user_id]++
|
||||
uid
|
||||
|
||||
# Retrieve an operation from a unique id.
|
||||
getOperation: (uid)->
|
||||
if uid instanceof Object
|
||||
@buffer[uid.creator]?[uid.op_number]
|
||||
else
|
||||
throw new Error "This type of uid is not defined!"
|
||||
|
||||
# Add an operation to the HB. Note that this will not link it against
|
||||
# other operations (it wont be executable)
|
||||
addOperation: (o)->
|
||||
if not @buffer[o.creator]?
|
||||
@buffer[o.creator] = {}
|
||||
if not @operation_counter[o.creator]?
|
||||
@operation_counter[o.creator] = 0
|
||||
#if @operation_counter[o.creator] isnt o.op_number and typeof o.op_number is 'number'
|
||||
# throw new Error "You don't receive operations in the proper order. Try counting like this 0,1,2,3,4,.. ;)"
|
||||
if @buffer[o.creator][o.op_number]?
|
||||
throw new Error "You must not overwrite operations!"
|
||||
@buffer[o.creator][o.op_number] = o
|
||||
if typeof o.op_number is 'number' and o.creator isnt @getUserId()
|
||||
@operation_counter[o.creator]++
|
||||
o
|
||||
|
||||
|
||||
|
||||
module.exports = HistoryBuffer
|
||||
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