Introduced a new model for custom collaborative types.
This commit is contained in:
parent
b647b2af58
commit
f1f710b269
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -11,18 +11,18 @@ class Engine
|
||||
|
||||
#
|
||||
# @param {HistoryBuffer} HB
|
||||
# @param {Array} parser Defines how to parse encoded messages.
|
||||
# @param {Object} types list of available types
|
||||
#
|
||||
constructor: (@HB, @parser)->
|
||||
constructor: (@HB, @types)->
|
||||
@unprocessed_ops = []
|
||||
|
||||
#
|
||||
# Parses an operatio from the json format. It uses the specified parser in your OperationType module.
|
||||
#
|
||||
parseOperation: (json)->
|
||||
typeParser = @parser[json.type]
|
||||
if typeParser?
|
||||
typeParser json
|
||||
type = @types[json.type]
|
||||
if type?.parse?
|
||||
type.parse json
|
||||
else
|
||||
throw new Error "You forgot to specify a parser for type #{json.type}. The message is #{JSON.stringify json}."
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
module.exports = (HB)->
|
||||
# @see Engine.parse
|
||||
parser = {}
|
||||
types = {}
|
||||
execution_listener = []
|
||||
|
||||
#
|
||||
@ -16,7 +16,7 @@ module.exports = (HB)->
|
||||
#
|
||||
# Furthermore an encodable operation has a parser. We extend the parser object in order to parse encoded operations.
|
||||
#
|
||||
class Operation
|
||||
class types.Operation
|
||||
|
||||
#
|
||||
# @param {Object} uid A unique identifier.
|
||||
@ -192,12 +192,11 @@ module.exports = (HB)->
|
||||
@unchecked = uninstantiated
|
||||
success
|
||||
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
# A simple Delete-type operation that deletes an operation.
|
||||
#
|
||||
class Delete extends Operation
|
||||
class types.Delete extends types.Operation
|
||||
|
||||
#
|
||||
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
||||
@ -237,12 +236,12 @@ module.exports = (HB)->
|
||||
#
|
||||
# Define how to parse Delete operations.
|
||||
#
|
||||
parser['Delete'] = (o)->
|
||||
types.Delete.parse = (o)->
|
||||
{
|
||||
'uid' : uid
|
||||
'deletes': deletes_uid
|
||||
} = o
|
||||
new Delete uid, deletes_uid
|
||||
new this(uid, deletes_uid)
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
@ -254,7 +253,7 @@ module.exports = (HB)->
|
||||
# - 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
|
||||
class types.Insert extends types.Operation
|
||||
|
||||
#
|
||||
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
||||
@ -422,7 +421,7 @@ module.exports = (HB)->
|
||||
position = 0
|
||||
prev = @prev_cl
|
||||
while true
|
||||
if prev instanceof Delimiter
|
||||
if prev instanceof types.Delimiter
|
||||
break
|
||||
if not prev.isDeleted()
|
||||
position++
|
||||
@ -433,7 +432,7 @@ module.exports = (HB)->
|
||||
# @nodoc
|
||||
# Defines an object that is cannot be changed. You can use this to set an immutable string, or a number.
|
||||
#
|
||||
class ImmutableObject extends Operation
|
||||
class types.ImmutableObject extends types.Operation
|
||||
|
||||
#
|
||||
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
||||
@ -455,18 +454,18 @@ module.exports = (HB)->
|
||||
#
|
||||
_encode: ()->
|
||||
json = {
|
||||
'type': "ImmutableObject"
|
||||
'type': @type
|
||||
'uid' : @getUid()
|
||||
'content' : @content
|
||||
}
|
||||
json
|
||||
|
||||
parser['ImmutableObject'] = (json)->
|
||||
types.ImmutableObject.parse = (json)->
|
||||
{
|
||||
'uid' : uid
|
||||
'content' : content
|
||||
} = json
|
||||
new ImmutableObject uid, content
|
||||
new this(uid, content)
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
@ -474,7 +473,7 @@ module.exports = (HB)->
|
||||
# This is necessary in order to have a beginning and an end even if the content
|
||||
# of the Engine is empty.
|
||||
#
|
||||
class Delimiter extends Operation
|
||||
class types.Delimiter extends types.Operation
|
||||
#
|
||||
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
||||
# @param {Operation} prev_cl The predecessor of this operation in the complete-list (cl)
|
||||
@ -527,29 +526,23 @@ module.exports = (HB)->
|
||||
#
|
||||
_encode: ()->
|
||||
{
|
||||
'type' : "Delimiter"
|
||||
'type' : @type
|
||||
'uid' : @getUid()
|
||||
'prev' : @prev_cl?.getUid()
|
||||
'next' : @next_cl?.getUid()
|
||||
}
|
||||
|
||||
parser['Delimiter'] = (json)->
|
||||
types.Delimiter.parse = (json)->
|
||||
{
|
||||
'uid' : uid
|
||||
'prev' : prev
|
||||
'next' : next
|
||||
} = json
|
||||
new Delimiter uid, prev, next
|
||||
new this(uid, prev, next)
|
||||
|
||||
# This is what this module exports after initializing it with the HistoryBuffer
|
||||
{
|
||||
'types' :
|
||||
'Delete' : Delete
|
||||
'Insert' : Insert
|
||||
'Delimiter': Delimiter
|
||||
'Operation': Operation
|
||||
'ImmutableObject' : ImmutableObject
|
||||
'parser' : parser
|
||||
'types' : types
|
||||
'execution_listener' : execution_listener
|
||||
}
|
||||
|
||||
|
@ -3,100 +3,11 @@ text_types_uninitialized = require "./TextTypes"
|
||||
module.exports = (HB)->
|
||||
text_types = text_types_uninitialized HB
|
||||
types = text_types.types
|
||||
parser = text_types.parser
|
||||
|
||||
createJsonTypeWrapper = (_jsonType)->
|
||||
|
||||
#
|
||||
# @note EXPERIMENTAL
|
||||
#
|
||||
# A JsonTypeWrapper was intended to be a convenient wrapper for the JsonType.
|
||||
# But it can make things more difficult than they are.
|
||||
# @see JsonType
|
||||
#
|
||||
# @example create a JsonTypeWrapper
|
||||
# # You get a JsonTypeWrapper from a JsonType by calling
|
||||
# w = yatta.value
|
||||
#
|
||||
# It creates Javascripts -getter and -setter methods for each property that JsonType maintains.
|
||||
# @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
|
||||
#
|
||||
# @example Getter Example
|
||||
# # you can access the x property of yatta by calling
|
||||
# w.x
|
||||
# # instead of
|
||||
# yatta.val('x')
|
||||
#
|
||||
# @note You can only overwrite existing values! Setting a new property won't have any effect!
|
||||
#
|
||||
# @example Setter Example
|
||||
# # you can set an existing x property of yatta by calling
|
||||
# w.x = "text"
|
||||
# # instead of
|
||||
# yatta.val('x', "text")
|
||||
#
|
||||
# In order to set a new property you have to overwrite an existing property.
|
||||
# Therefore the JsonTypeWrapper supports a special feature that should make things more convenient
|
||||
# (we can argue about that, use the JsonType if you don't like it ;).
|
||||
# If you overwrite an object property of the JsonTypeWrapper with a new object, it will result in a merged version of the objects.
|
||||
# Let `yatta.value.p` the property that is to be overwritten and o the new value. E.g. `yatta.value.p = o`
|
||||
# * The result has all properties of o
|
||||
# * The result has all properties of w.p if they don't occur under the same property-name in o.
|
||||
#
|
||||
# @example Conflict Example
|
||||
# yatta.value = {a : "string"}
|
||||
# w = yatta.value
|
||||
# console.log(w) # {a : "string"}
|
||||
# w.a = {a : {b : "string"}}
|
||||
# console.log(w) # {a : {b : "String"}}
|
||||
# w.a = {a : {c : 4}}
|
||||
# console.log(w) # {a : {b : "String", c : 4}}
|
||||
#
|
||||
# @example Common Pitfalls
|
||||
# w = yatta.value
|
||||
# # Setting a new property
|
||||
# w.newProperty = "Awesome"
|
||||
# console.log(w.newProperty == "Awesome") # false, w.newProperty is undefined
|
||||
# # overwrite the w object
|
||||
# w = {newProperty : "Awesome"}
|
||||
# console.log(w.newProperty == "Awesome") # true!, but ..
|
||||
# console.log(yatta.value.newProperty == "Awesome") # false, you are only allowed to set properties!
|
||||
# # The solution
|
||||
# yatta.value = {newProperty : "Awesome"}
|
||||
# console.log(w.newProperty == "Awesome") # true!
|
||||
#
|
||||
class JsonTypeWrapper
|
||||
|
||||
#
|
||||
# @param {JsonType} jsonType Instance of the JsonType that this class wrappes.
|
||||
#
|
||||
constructor: (jsonType)->
|
||||
for name, obj of jsonType.map
|
||||
do (name, obj)->
|
||||
Object.defineProperty JsonTypeWrapper.prototype, name,
|
||||
get : ->
|
||||
x = obj.val()
|
||||
if x instanceof JsonType
|
||||
createJsonTypeWrapper x
|
||||
else if x instanceof types.ImmutableObject
|
||||
x.val()
|
||||
else
|
||||
x
|
||||
set : (o)->
|
||||
overwrite = jsonType.val(name)
|
||||
if o.constructor is {}.constructor and overwrite instanceof types.Operation
|
||||
for o_name,o_obj of o
|
||||
overwrite.val(o_name, o_obj, 'immutable')
|
||||
else
|
||||
jsonType.val(name, o, 'immutable')
|
||||
enumerable: true
|
||||
configurable: false
|
||||
new JsonTypeWrapper _jsonType
|
||||
|
||||
#
|
||||
# Manages Object-like values.
|
||||
#
|
||||
class JsonType extends types.MapManager
|
||||
class types.Object extends types.MapManager
|
||||
|
||||
#
|
||||
# Identifies this class.
|
||||
@ -104,11 +15,11 @@ module.exports = (HB)->
|
||||
#
|
||||
# @example
|
||||
# var x = yatta.val('unknown')
|
||||
# if (x.type === "JsonType") {
|
||||
# if (x.type === "Object") {
|
||||
# console.log JSON.stringify(x.toJson())
|
||||
# }
|
||||
#
|
||||
type: "JsonType"
|
||||
type: "Object"
|
||||
|
||||
applyDelete: ()->
|
||||
super()
|
||||
@ -130,7 +41,7 @@ module.exports = (HB)->
|
||||
val = @val()
|
||||
json = {}
|
||||
for name, o of val
|
||||
if o instanceof JsonType
|
||||
if o instanceof types.Object
|
||||
json[name] = o.toJson(transform_to_value)
|
||||
else if transform_to_value and o instanceof types.Operation
|
||||
json[name] = o.val()
|
||||
@ -171,24 +82,6 @@ module.exports = (HB)->
|
||||
changedBy:event.changedBy
|
||||
@bound_json
|
||||
|
||||
#
|
||||
# Whether the default is 'mutable' (true) or 'immutable' (false)
|
||||
#
|
||||
mutable_default:
|
||||
false
|
||||
|
||||
#
|
||||
# Set if the default is 'mutable' or 'immutable'
|
||||
# @param {String|Boolean} mutable Set either 'mutable' / true or 'immutable' / false
|
||||
setMutableDefault: (mutable)->
|
||||
if mutable is true or mutable is 'mutable'
|
||||
JsonType.prototype.mutable_default = true
|
||||
else if mutable is false or mutable is 'immutable'
|
||||
JsonType.prototype.mutable_default = false
|
||||
else
|
||||
throw new Error 'Set mutable either "mutable" or "immutable"!'
|
||||
'OK'
|
||||
|
||||
#
|
||||
# @overload val()
|
||||
# Get this as a Json object.
|
||||
@ -197,70 +90,56 @@ module.exports = (HB)->
|
||||
# @overload val(name)
|
||||
# Get value of a property.
|
||||
# @param {String} name Name of the object property.
|
||||
# @return [JsonType|WordType|String|Object] Depending on the value of the property. If mutable it will return a Operation-type object, if immutable it will return String/Object.
|
||||
# @return [Object Type||String|Object] Depending on the value of the property. If mutable it will return a Operation-type object, if immutable it will return String/Object.
|
||||
#
|
||||
# @overload val(name, content)
|
||||
# Set a new property.
|
||||
# @param {String} name Name of the object property.
|
||||
# @param {Object|String} content Content of the object property.
|
||||
# @return [JsonType] This object. (supports chaining)
|
||||
# @return [Object Type] This object. (supports chaining)
|
||||
#
|
||||
val: (name, content, mutable)->
|
||||
val: (name, content)->
|
||||
if name? and arguments.length > 1
|
||||
if mutable?
|
||||
if mutable is true or mutable is 'mutable'
|
||||
mutable = true
|
||||
if content? and content.constructor?
|
||||
type = types[content.constructor.name]
|
||||
if type? and type.create?
|
||||
args = []
|
||||
for i in [1...arguments.length]
|
||||
args.push arguments[i]
|
||||
o = type.create.apply null, args
|
||||
super name, o
|
||||
else
|
||||
mutable = false
|
||||
throw new Error "The #{content.constructor.name}-type is not (yet) supported in Yatta."
|
||||
else
|
||||
mutable = @mutable_default
|
||||
if typeof content is 'function'
|
||||
@ # Just do nothing
|
||||
else if (not content?) or (((not mutable) or typeof content is 'number') and content.constructor isnt Object)
|
||||
super name, (new types.ImmutableObject undefined, content).execute()
|
||||
else
|
||||
if typeof content is 'string'
|
||||
word = (new types.WordType undefined).execute()
|
||||
word.insertText 0, content
|
||||
super name, word
|
||||
else if content.constructor is Object
|
||||
json = new JsonType().execute()
|
||||
for n,o of content
|
||||
json.val n, o, mutable
|
||||
super name, json
|
||||
else
|
||||
throw new Error "You must not set #{typeof content}-types in collaborative Json-objects!"
|
||||
else
|
||||
super name, content
|
||||
|
||||
Object.defineProperty JsonType.prototype, 'value',
|
||||
get : -> createJsonTypeWrapper @
|
||||
set : (o)->
|
||||
if o.constructor is {}.constructor
|
||||
for o_name,o_obj of o
|
||||
@val(o_name, o_obj, 'immutable')
|
||||
else
|
||||
throw new Error "You must only set Object values!"
|
||||
else # is this even necessary ? I have to define every type anyway.. (see Number type below)
|
||||
super name
|
||||
|
||||
#
|
||||
# @private
|
||||
#
|
||||
_encode: ()->
|
||||
{
|
||||
'type' : "JsonType"
|
||||
'type' : @type
|
||||
'uid' : @getUid()
|
||||
}
|
||||
|
||||
parser['JsonType'] = (json)->
|
||||
types.Object.parse = (json)->
|
||||
{
|
||||
'uid' : uid
|
||||
} = json
|
||||
new JsonType uid
|
||||
new this(uid)
|
||||
|
||||
types.Object.create = (content, mutable)->
|
||||
json = new types.Object().execute()
|
||||
for n,o of content
|
||||
json.val n, o, mutable
|
||||
json
|
||||
|
||||
|
||||
|
||||
|
||||
types['JsonType'] = JsonType
|
||||
types.Number = {}
|
||||
types.Number.create = (content)->
|
||||
(new types.ImmutableObject undefined, content).execute()
|
||||
|
||||
text_types
|
||||
|
||||
|
@ -3,13 +3,12 @@ basic_types_uninitialized = require "./BasicTypes"
|
||||
module.exports = (HB)->
|
||||
basic_types = basic_types_uninitialized HB
|
||||
types = basic_types.types
|
||||
parser = basic_types.parser
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
# Manages map like objects. E.g. Json-Type and XML attributes.
|
||||
#
|
||||
class MapManager extends types.Operation
|
||||
class types.MapManager extends types.Operation
|
||||
|
||||
#
|
||||
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
||||
@ -69,7 +68,7 @@ module.exports = (HB)->
|
||||
rm_uid =
|
||||
noOperation: true
|
||||
alt: map_uid
|
||||
rm = new ReplaceManager event_properties, event_this, rm_uid # this operation shall not be saved in the HB
|
||||
rm = new types.ReplaceManager event_properties, event_this, rm_uid # this operation shall not be saved in the HB
|
||||
@map[property_name] = rm
|
||||
rm.setParent @, property_name
|
||||
rm.execute()
|
||||
@ -79,7 +78,7 @@ module.exports = (HB)->
|
||||
# @nodoc
|
||||
# Manages a list of Insert-type operations.
|
||||
#
|
||||
class ListManager extends types.Operation
|
||||
class types.ListManager extends types.Operation
|
||||
|
||||
#
|
||||
# A ListManager maintains a non-empty list that has a beginning and an end (both Delimiters!)
|
||||
@ -156,10 +155,10 @@ module.exports = (HB)->
|
||||
# Adds support for replace. The ReplaceManager manages Replaceable operations.
|
||||
# Each Replaceable holds a value that is now replaceable.
|
||||
#
|
||||
# The WordType-type has implemented support for replace
|
||||
# @see WordType
|
||||
# The TextType-type has implemented support for replace
|
||||
# @see TextType
|
||||
#
|
||||
class ReplaceManager extends ListManager
|
||||
class types.ReplaceManager extends types.ListManager
|
||||
#
|
||||
# @param {Object} event_properties Decorates the event that is thrown by the RM
|
||||
# @param {Object} event_this The object on which the event shall be executed
|
||||
@ -207,7 +206,7 @@ module.exports = (HB)->
|
||||
#
|
||||
replace: (content, replaceable_uid)->
|
||||
o = @getLastOperation()
|
||||
relp = (new Replaceable content, @, replaceable_uid, o, o.next_cl).execute()
|
||||
relp = (new types.Replaceable content, @, replaceable_uid, o, o.next_cl).execute()
|
||||
# TODO: delete repl (for debugging)
|
||||
undefined
|
||||
|
||||
@ -219,7 +218,7 @@ module.exports = (HB)->
|
||||
undefined
|
||||
|
||||
#
|
||||
# Get the value of this WordType
|
||||
# Get the value of this
|
||||
# @return {String}
|
||||
#
|
||||
val: ()->
|
||||
@ -234,7 +233,7 @@ module.exports = (HB)->
|
||||
_encode: ()->
|
||||
json =
|
||||
{
|
||||
'type': "ReplaceManager"
|
||||
'type': @type
|
||||
'uid' : @getUid()
|
||||
'beginning' : @beginning.getUid()
|
||||
'end' : @end.getUid()
|
||||
@ -246,7 +245,7 @@ module.exports = (HB)->
|
||||
# The ReplaceManager manages Replaceables.
|
||||
# @see ReplaceManager
|
||||
#
|
||||
class Replaceable extends types.Insert
|
||||
class types.Replaceable extends types.Insert
|
||||
|
||||
#
|
||||
# @param {Operation} content The value that this Replaceable holds.
|
||||
@ -321,7 +320,7 @@ module.exports = (HB)->
|
||||
_encode: ()->
|
||||
json =
|
||||
{
|
||||
'type': "Replaceable"
|
||||
'type': @type
|
||||
'content': @content?.getUid()
|
||||
'parent' : @parent.getUid()
|
||||
'prev': @prev_cl.getUid()
|
||||
@ -332,7 +331,7 @@ module.exports = (HB)->
|
||||
}
|
||||
json
|
||||
|
||||
parser["Replaceable"] = (json)->
|
||||
types.Replaceable.parse = (json)->
|
||||
{
|
||||
'content' : content
|
||||
'parent' : parent
|
||||
@ -342,12 +341,8 @@ module.exports = (HB)->
|
||||
'origin' : origin
|
||||
'is_deleted': is_deleted
|
||||
} = json
|
||||
new Replaceable content, parent, uid, prev, next, origin, is_deleted
|
||||
new this(content, parent, uid, prev, next, origin, is_deleted)
|
||||
|
||||
types['ListManager'] = ListManager
|
||||
types['MapManager'] = MapManager
|
||||
types['ReplaceManager'] = ReplaceManager
|
||||
types['Replaceable'] = Replaceable
|
||||
|
||||
basic_types
|
||||
|
||||
|
@ -5,19 +5,11 @@ module.exports = (HB)->
|
||||
types = structured_types.types
|
||||
parser = structured_types.parser
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
# At the moment TextDelete type equals the Delete type in BasicTypes.
|
||||
# @see BasicTypes.Delete
|
||||
#
|
||||
class TextDelete extends types.Delete
|
||||
parser["TextDelete"] = parser["Delete"]
|
||||
|
||||
#
|
||||
# @nodoc
|
||||
# Extends the basic Insert type to an operation that holds a text value
|
||||
#
|
||||
class TextInsert extends types.Insert
|
||||
class types.TextInsert extends types.Insert
|
||||
#
|
||||
# @param {String} content The content of this Insert-type Operation. Usually you restrict the length of content to size 1
|
||||
# @param {Object} uid A unique identifier. If uid is undefined, a new uid will be created.
|
||||
@ -72,7 +64,7 @@ module.exports = (HB)->
|
||||
_encode: ()->
|
||||
json =
|
||||
{
|
||||
'type': "TextInsert"
|
||||
'type': @type
|
||||
'uid' : @getUid()
|
||||
'prev': @prev_cl.getUid()
|
||||
'next': @next_cl.getUid()
|
||||
@ -86,7 +78,7 @@ module.exports = (HB)->
|
||||
json['content'] = @content
|
||||
json
|
||||
|
||||
parser["TextInsert"] = (json)->
|
||||
types.TextInsert.parse = (json)->
|
||||
{
|
||||
'content' : content
|
||||
'uid' : uid
|
||||
@ -95,13 +87,109 @@ module.exports = (HB)->
|
||||
'origin' : origin
|
||||
'parent' : parent
|
||||
} = json
|
||||
new TextInsert content, uid, prev, next, origin, parent
|
||||
new types.TextInsert content, uid, prev, next, origin, parent
|
||||
|
||||
|
||||
class types.Array extends types.ListManager
|
||||
|
||||
type: "Array"
|
||||
|
||||
applyDelete: ()->
|
||||
o = @end
|
||||
while o?
|
||||
o.applyDelete()
|
||||
o = o.prev_cl
|
||||
super()
|
||||
|
||||
cleanup: ()->
|
||||
super()
|
||||
|
||||
val: ()->
|
||||
o = @beginning.next_cl
|
||||
result = []
|
||||
while o isnt @end
|
||||
result.push o.val()
|
||||
o = o.next_cl
|
||||
result
|
||||
|
||||
push: (content)->
|
||||
@insertAfter @end.prev_cl, content
|
||||
|
||||
insertAfter: (left, content)->
|
||||
right = left.next_cl
|
||||
while right.isDeleted()
|
||||
right = right.next_cl # find the first character to the right, that is not deleted. In the case that position is 0, its the Delimiter.
|
||||
left = right.prev_cl
|
||||
if content.type?
|
||||
(new types.TextInsert content, undefined, left, right).execute()
|
||||
else
|
||||
for c in content
|
||||
tmp = (new types.TextInsert c, undefined, left, right).execute()
|
||||
left = tmp
|
||||
@
|
||||
|
||||
#
|
||||
# Inserts a string into the word.
|
||||
#
|
||||
# @return {Array Type} This String object.
|
||||
#
|
||||
insert: (position, content)->
|
||||
ith = @getOperationByPosition position
|
||||
# the (i-1)th character. e.g. "abc" the 1th character is "a"
|
||||
# the 0th character is the left Delimiter
|
||||
@insertAfter ith, content
|
||||
|
||||
#
|
||||
# Deletes a part of the word.
|
||||
#
|
||||
# @return {Array Type} This String object
|
||||
#
|
||||
delete: (position, length)->
|
||||
o = @getOperationByPosition(position+1) # position 0 in this case is the deletion of the first character
|
||||
|
||||
delete_ops = []
|
||||
for i in [0...length]
|
||||
if o instanceof types.Delimiter
|
||||
break
|
||||
d = (new types.Delete undefined, o).execute()
|
||||
o = o.next_cl
|
||||
while not (o instanceof types.Delimiter) and o.isDeleted()
|
||||
o = o.next_cl
|
||||
delete_ops.push d._encode()
|
||||
@
|
||||
|
||||
#
|
||||
# @private
|
||||
# Encode this operation in such a way that it can be parsed by remote peers.
|
||||
#
|
||||
_encode: ()->
|
||||
json = {
|
||||
'type': @type
|
||||
'uid' : @getUid()
|
||||
}
|
||||
json
|
||||
|
||||
types.Array.parse = (json)->
|
||||
{
|
||||
'uid' : uid
|
||||
} = json
|
||||
new this(uid)
|
||||
|
||||
types.Array.create = (content, mutable)->
|
||||
if (mutable is "mutable")
|
||||
list = new types.Array().execute()
|
||||
list.insert 0, content
|
||||
list
|
||||
else if (not mutable?) or (mutable is "immutable")
|
||||
(new types.ImmutableObject undefined, content).execute()
|
||||
else
|
||||
throw new Error "Specify either \"mutable\" or \"immutable\"!!"
|
||||
|
||||
#
|
||||
# Handles a WordType-like data structures with support for insertText/deleteText at a word-position.
|
||||
# Handles a String-like data structures with support for insert/delete at a word-position.
|
||||
# @note Currently, only Text is supported!
|
||||
#
|
||||
class WordType extends types.ListManager
|
||||
class types.String extends types.Array
|
||||
|
||||
#
|
||||
# @private
|
||||
@ -117,66 +205,11 @@ module.exports = (HB)->
|
||||
#
|
||||
# @example
|
||||
# var x = yatta.val('unknown')
|
||||
# if (x.type === "WordType") {
|
||||
# if (x.type === "String") {
|
||||
# console.log JSON.stringify(x.toJson())
|
||||
# }
|
||||
#
|
||||
type: "WordType"
|
||||
|
||||
applyDelete: ()->
|
||||
o = @end
|
||||
while o?
|
||||
o.applyDelete()
|
||||
o = o.prev_cl
|
||||
super()
|
||||
|
||||
cleanup: ()->
|
||||
super()
|
||||
|
||||
push: (content)->
|
||||
@insertAfter @end.prev_cl, content
|
||||
|
||||
insertAfter: (left, content)->
|
||||
right = left.next_cl
|
||||
while right.isDeleted()
|
||||
right = right.next_cl # find the first character to the right, that is not deleted. In the case that position is 0, its the Delimiter.
|
||||
left = right.prev_cl
|
||||
if content.type?
|
||||
(new TextInsert content, undefined, left, right).execute()
|
||||
else
|
||||
for c in content
|
||||
tmp = (new TextInsert c, undefined, left, right).execute()
|
||||
left = tmp
|
||||
@
|
||||
#
|
||||
# Inserts a string into the word.
|
||||
#
|
||||
# @return {WordType} This WordType object.
|
||||
#
|
||||
insertText: (position, content)->
|
||||
ith = @getOperationByPosition position
|
||||
# the (i-1)th character. e.g. "abc" the 1th character is "a"
|
||||
# the 0th character is the left Delimiter
|
||||
@insertAfter ith, content
|
||||
|
||||
#
|
||||
# Deletes a part of the word.
|
||||
#
|
||||
# @return {WordType} This WordType object
|
||||
#
|
||||
deleteText: (position, length)->
|
||||
o = @getOperationByPosition(position+1) # position 0 in this case is the deletion of the first character
|
||||
|
||||
delete_ops = []
|
||||
for i in [0...length]
|
||||
if o instanceof types.Delimiter
|
||||
break
|
||||
d = (new TextDelete undefined, o).execute()
|
||||
o = o.next_cl
|
||||
while not (o instanceof types.Delimiter) and o.isDeleted()
|
||||
o = o.next_cl
|
||||
delete_ops.push d._encode()
|
||||
@
|
||||
type: "String"
|
||||
|
||||
#
|
||||
# Get the String-representation of this word.
|
||||
@ -191,14 +224,14 @@ module.exports = (HB)->
|
||||
c.join('')
|
||||
|
||||
#
|
||||
# Same as WordType.val
|
||||
# @see WordType.val
|
||||
# Same as String.val
|
||||
# @see String.val
|
||||
#
|
||||
toString: ()->
|
||||
@val()
|
||||
|
||||
#
|
||||
# Bind this WordType to a textfield or input field.
|
||||
# Bind this String to a textfield or input field.
|
||||
#
|
||||
# @example
|
||||
# var textbox = document.getElementById("textfield");
|
||||
@ -257,8 +290,8 @@ module.exports = (HB)->
|
||||
if char.length > 0
|
||||
pos = Math.min textfield.selectionStart, textfield.selectionEnd
|
||||
diff = Math.abs(textfield.selectionEnd - textfield.selectionStart)
|
||||
word.deleteText (pos), diff
|
||||
word.insertText pos, char
|
||||
word.delete (pos), diff
|
||||
word.insert pos, char
|
||||
new_pos = pos + char.length
|
||||
textfield.setSelectionRange new_pos, new_pos
|
||||
event.preventDefault()
|
||||
@ -294,7 +327,7 @@ module.exports = (HB)->
|
||||
diff = Math.abs(textfield.selectionEnd - textfield.selectionStart)
|
||||
if event.keyCode? and event.keyCode is 8 # Backspace
|
||||
if diff > 0
|
||||
word.deleteText pos, diff
|
||||
word.delete pos, diff
|
||||
textfield.setSelectionRange pos, pos
|
||||
else
|
||||
if event.ctrlKey? and event.ctrlKey
|
||||
@ -307,42 +340,48 @@ module.exports = (HB)->
|
||||
while new_pos > 0 and val[new_pos] isnt " " and val[new_pos] isnt '\n'
|
||||
new_pos--
|
||||
del_length++
|
||||
word.deleteText new_pos, (pos-new_pos)
|
||||
word.delete new_pos, (pos-new_pos)
|
||||
textfield.setSelectionRange new_pos, new_pos
|
||||
else
|
||||
word.deleteText (pos-1), 1
|
||||
word.delete (pos-1), 1
|
||||
event.preventDefault()
|
||||
else if event.keyCode? and event.keyCode is 46 # Delete
|
||||
if diff > 0
|
||||
word.deleteText pos, diff
|
||||
word.delete pos, diff
|
||||
textfield.setSelectionRange pos, pos
|
||||
else
|
||||
word.deleteText pos, 1
|
||||
word.delete pos, 1
|
||||
textfield.setSelectionRange pos, pos
|
||||
event.preventDefault()
|
||||
|
||||
|
||||
|
||||
#
|
||||
# @private
|
||||
# Encode this operation in such a way that it can be parsed by remote peers.
|
||||
#
|
||||
_encode: ()->
|
||||
json = {
|
||||
'type': "WordType"
|
||||
'type': @type
|
||||
'uid' : @getUid()
|
||||
}
|
||||
json
|
||||
|
||||
parser['WordType'] = (json)->
|
||||
types.String.parse = (json)->
|
||||
{
|
||||
'uid' : uid
|
||||
} = json
|
||||
new WordType uid
|
||||
new this(uid)
|
||||
|
||||
types.String.create = (content, mutable)->
|
||||
if (mutable is "mutable")
|
||||
word = new types.String().execute()
|
||||
word.insert 0, content
|
||||
word
|
||||
else if (not mutable?) or (mutable is "immutable")
|
||||
(new types.ImmutableObject undefined, content).execute()
|
||||
else
|
||||
throw new Error "Specify either \"mutable\" or \"immutable\"!!"
|
||||
|
||||
|
||||
types['TextInsert'] = TextInsert
|
||||
types['TextDelete'] = TextDelete
|
||||
types['WordType'] = WordType
|
||||
structured_types
|
||||
|
||||
|
||||
|
@ -24,7 +24,7 @@ createYatta = (connector)->
|
||||
# * Integer
|
||||
# * Array
|
||||
#
|
||||
class Yatta extends types.JsonType
|
||||
class Yatta extends types.Object
|
||||
|
||||
#
|
||||
# @param {String} user_id Unique id of the peer.
|
||||
@ -34,7 +34,7 @@ createYatta = (connector)->
|
||||
@connector = connector
|
||||
@HB = HB
|
||||
@types = types
|
||||
@engine = new Engine @HB, type_manager.parser
|
||||
@engine = new Engine @HB, type_manager.types
|
||||
adaptConnector @connector, @engine, @HB, type_manager.execution_listener
|
||||
super
|
||||
|
||||
|
@ -45,19 +45,19 @@ class JsonTest extends Test
|
||||
f : (y)=> # SET PROPERTY
|
||||
y.val(@getRandomKey(), @getRandomText(), 'immutable')
|
||||
null
|
||||
types : [types.JsonType]
|
||||
types : [types.Object]
|
||||
,
|
||||
f : (y)=> # SET Object Property 1)
|
||||
y.val(@getRandomObject())
|
||||
types: [types.JsonType]
|
||||
types: [types.Object]
|
||||
,
|
||||
f : (y)=> # SET Object Property 2)
|
||||
y.val(@getRandomKey(), @getRandomObject())
|
||||
types: [types.JsonType]
|
||||
types: [types.Object]
|
||||
,
|
||||
f : (y)=> # SET PROPERTY TEXT
|
||||
y.val(@getRandomKey(), @getRandomText(), 'mutable')
|
||||
types: [types.JsonType]
|
||||
types: [types.Object]
|
||||
]
|
||||
|
||||
describe "JsonFramework", ->
|
||||
@ -73,30 +73,6 @@ describe "JsonFramework", ->
|
||||
console.log "" # TODO
|
||||
@yTest.run()
|
||||
|
||||
### TODO
|
||||
it "has a update listener", ()->
|
||||
addName = false
|
||||
change = false
|
||||
change2 = 0
|
||||
@test_user.on 'add', (eventname, property_name)->
|
||||
if property_name is 'x'
|
||||
addName = true
|
||||
@test_user.val('x',5)
|
||||
@test_user.on 'change', (eventname, property_name)->
|
||||
if property_name is 'x'
|
||||
change = true
|
||||
@test_user.val('x', 6)
|
||||
@test_user.val('ins', "text", 'mutable')
|
||||
@test_user.on 'update', (eventname, property_name)->
|
||||
if property_name is 'ins'
|
||||
change2++
|
||||
@test_user.val('ins').insertText 4, " yay"
|
||||
@test_user.val('ins').deleteText 0, 4
|
||||
expect(addName).to.be.ok
|
||||
expect(change).to.be.ok
|
||||
expect(change2).to.equal 8
|
||||
###
|
||||
|
||||
it "has a working test suite", ->
|
||||
@yTest.compareAll()
|
||||
|
||||
@ -119,9 +95,9 @@ describe "JsonFramework", ->
|
||||
@yTest.users[1].val('a', 't', "mutable")
|
||||
@yTest.compareAll()
|
||||
q = @yTest.users[2].val('a')
|
||||
q.insertText(0,'A')
|
||||
q.insert(0,'A')
|
||||
@yTest.compareAll()
|
||||
expect(@yTest.getSomeUser().value.a.val()).to.equal("At")
|
||||
expect(@yTest.getSomeUser().val("a").val()).to.equal("At")
|
||||
|
||||
it "can handle creaton of complex json (2)", ->
|
||||
@yTest.getSomeUser().val('x', {'a':'b'})
|
||||
@ -130,10 +106,18 @@ describe "JsonFramework", ->
|
||||
@yTest.getSomeUser().val('c', {'a':'c'})
|
||||
@yTest.getSomeUser().val('c', {'a':'b'})
|
||||
@yTest.compareAll()
|
||||
q = @yTest.getSomeUser().value.a.a.q
|
||||
q.insertText(0,'A')
|
||||
q = @yTest.getSomeUser().val("a").val("a").val("q")
|
||||
q.insert(0,'A')
|
||||
@yTest.compareAll()
|
||||
expect(@yTest.getSomeUser().val("a").val("a").val("q").val()).to.equal("Adtrndtrtdrntdrnrtdnrtdnrtdnrtdnrdnrdt")
|
||||
|
||||
it "can handle creaton of complex json (3)", ->
|
||||
@yTest.users[0].val('l', [1,2,3], "mutable")
|
||||
@yTest.users[1].val('l', [4,5,6], "mutable")
|
||||
@yTest.compareAll()
|
||||
@yTest.users[2].val('l').insert(0,'A')
|
||||
@yTest.users[1].val('l').insert(0,'B')
|
||||
@yTest.compareAll()
|
||||
expect(@yTest.getSomeUser().value.a.a.q.val()).to.equal("Adtrndtrtdrntdrnrtdnrtdnrtdnrtdnrdnrdt")
|
||||
|
||||
it "handles immutables and primitive data types", ->
|
||||
@yTest.getSomeUser().val('string', "text", "immutable")
|
||||
|
@ -12,9 +12,9 @@ Connector = require "../bower_components/connector/lib/test-connector/test-conne
|
||||
module.exports = class Test
|
||||
constructor: (@name_suffix = "")->
|
||||
@number_of_test_cases_multiplier = 1
|
||||
@repeat_this = 503 * @number_of_test_cases_multiplier
|
||||
@doSomething_amount = 5 * @number_of_test_cases_multiplier
|
||||
@number_of_engines = 3 + @number_of_test_cases_multiplier - 1
|
||||
@repeat_this = 3 * @number_of_test_cases_multiplier
|
||||
@doSomething_amount = 123 * @number_of_test_cases_multiplier
|
||||
@number_of_engines = 5 + @number_of_test_cases_multiplier - 1
|
||||
|
||||
@time = 0 # denotes to the time when run was started
|
||||
@ops = 0 # number of operations (used with @time)
|
||||
@ -31,6 +31,7 @@ module.exports = class Test
|
||||
for user in @users
|
||||
u.getConnector().join(user.getConnector()) # TODO: change the test-connector to make this more convenient
|
||||
@users.push u
|
||||
@initUsers?(@users[0])
|
||||
@flushAll()
|
||||
|
||||
# is called by implementing class
|
||||
@ -74,17 +75,17 @@ module.exports = class Test
|
||||
f : (y)=> # INSERT TEXT
|
||||
y
|
||||
pos = _.random 0, (y.val().length-1)
|
||||
y.insertText pos, @getRandomText()
|
||||
y.insert pos, @getRandomText()
|
||||
null
|
||||
types: [types.WordType]
|
||||
types: [types.String]
|
||||
,
|
||||
f : (y)-> # DELETE TEXT
|
||||
if y.val().length > 0
|
||||
pos = _.random 0, (y.val().length-1)
|
||||
pos = _.random 0, (y.val().length-1) # TODO: put here also arbitrary number (test behaviour in error cases)
|
||||
length = _.random 0, (y.val().length - pos)
|
||||
ops1 = y.deleteText pos, length
|
||||
ops1 = y.delete pos, length
|
||||
undefined
|
||||
types : [types.WordType]
|
||||
types : [types.String]
|
||||
]
|
||||
getRandomRoot: (user_num)->
|
||||
throw new Error "overwrite me!"
|
||||
@ -99,7 +100,7 @@ module.exports = class Test
|
||||
y instanceof type
|
||||
|
||||
if choices.length is 0
|
||||
throw new Error "You forgot to specify a test generation methot for this Operation!"
|
||||
throw new Error "You forgot to specify a test generation methot for this Operation! (#{y.type})"
|
||||
i = _.random 0, (choices.length-1)
|
||||
choices[i].f y
|
||||
|
||||
|
@ -19,6 +19,9 @@ class TextTest extends Test
|
||||
conn = new Connector userId
|
||||
new Yatta conn
|
||||
|
||||
initUsers: (u)->
|
||||
u.val("TextTest","","mutable")
|
||||
|
||||
getRandomRoot: (user_num)->
|
||||
@users[user_num].val("TextTest")
|
||||
|
||||
@ -29,21 +32,17 @@ describe "TextFramework", ->
|
||||
beforeEach (done)->
|
||||
@timeout 50000
|
||||
@yTest = new TextTest()
|
||||
@users = @yTest.users
|
||||
test_user_connector = new Connector 'test_user'
|
||||
@test_user = @yTest.makeNewUser 'test_user', test_user_connector
|
||||
test_user_connector.join @users[0].connector
|
||||
@users[0].val("TextTest","","mutable")
|
||||
@yTest.flushAll()
|
||||
done()
|
||||
|
||||
it "simple multi-char insert", ->
|
||||
u = @yTest.users[0].val("TextTest")
|
||||
u.insertText 0, "abc"
|
||||
u.insert 0, "abc"
|
||||
u = @yTest.users[1].val("TextTest")
|
||||
u.insertText 0, "xyz"
|
||||
u.insert 0, "xyz"
|
||||
@yTest.compareAll()
|
||||
expect(u.val()).to.equal("abcxyz")
|
||||
u.delete 0, 1
|
||||
@yTest.compareAll()
|
||||
expect(u.val()).to.equal("bcxyz")
|
||||
|
||||
it "Observers work on shared Text (insert type observers, local and foreign)", ->
|
||||
u = @yTest.users[0].val("TextTest","my awesome Text","mutable").val("TextTest")
|
||||
@ -59,7 +58,7 @@ describe "TextFramework", ->
|
||||
expect(change.changedBy).to.equal('0')
|
||||
last_task = "observer1"
|
||||
u.observe observer1
|
||||
u.insertText 1, "a"
|
||||
u.insert 1, "a"
|
||||
expect(last_task).to.equal("observer1")
|
||||
u.unobserve observer1
|
||||
|
||||
@ -74,7 +73,7 @@ describe "TextFramework", ->
|
||||
last_task = "observer2"
|
||||
u.observe observer2
|
||||
v = @yTest.users[1].val("TextTest")
|
||||
v.insertText 0, "x"
|
||||
v.insert 0, "x"
|
||||
@yTest.flushAll()
|
||||
expect(last_task).to.equal("observer2")
|
||||
u.unobserve observer2
|
||||
@ -93,7 +92,7 @@ describe "TextFramework", ->
|
||||
expect(change.changedBy).to.equal('0')
|
||||
last_task = "observer1"
|
||||
u.observe observer1
|
||||
u.deleteText 1, 1
|
||||
u.delete 1, 1
|
||||
expect(last_task).to.equal("observer1")
|
||||
u.unobserve observer1
|
||||
|
||||
@ -108,7 +107,7 @@ describe "TextFramework", ->
|
||||
last_task = "observer2"
|
||||
u.observe observer2
|
||||
v = @yTest.users[1].val("TextTest")
|
||||
v.deleteText 0, 1
|
||||
v.delete 0, 1
|
||||
@yTest.flushAll()
|
||||
expect(last_task).to.equal("observer2")
|
||||
u.unobserve observer2
|
||||
|
File diff suppressed because one or more lines are too long
Loading…
x
Reference in New Issue
Block a user