more refactoring

This commit is contained in:
Kevin Jahns
2025-12-29 11:37:01 +01:00
parent 69131b76c8
commit ec11ef6f08
30 changed files with 506 additions and 4597 deletions

2
package-lock.json generated
View File

@@ -30,7 +30,7 @@
}
},
"../lib0": {
"version": "0.2.115",
"version": "0.2.116",
"license": "MIT",
"bin": {
"0ecdsa-generate-keypair": "src/bin/0ecdsa-generate-keypair.js",

View File

@@ -3,13 +3,7 @@
export {
Doc,
Transaction,
YArray as Array,
YMap as Map,
YText as Text,
YXmlText as XmlText,
YXmlHook as XmlHook,
YXmlElement as XmlElement,
YXmlFragment as XmlFragment,
YType as Type,
YEvent,
Item,
AbstractStruct,
@@ -46,7 +40,6 @@ export {
getItem,
getItemCleanStart,
getItemCleanEnd,
typeListToArraySnapshot,
typeMapGetSnapshot,
typeMapGetAllSnapshot,
createDocFromSnapshot,
@@ -72,7 +65,6 @@ export {
equalSnapshots,
tryGc,
transact,
AbstractConnector,
logType,
mergeUpdates,
mergeUpdatesV2,

View File

@@ -1,4 +1,3 @@
export * from './utils/AbstractConnector.js'
export * from './utils/IdSet.js'
export * from './utils/Doc.js'
export * from './utils/UpdateDecoder.js'

View File

@@ -1,5 +1,5 @@
import {
YText, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, StructStore, Transaction // eslint-disable-line
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, Transaction // eslint-disable-line
} from '../internals.js'
import * as error from 'lib0/error'
@@ -67,7 +67,7 @@ export class ContentFormat {
*/
integrate (_transaction, item) {
// @todo searchmarker are currently unsupported for rich text documents
const p = /** @type {YText<any>} */ (item.parent)
const p = /** @type {import('../ytype.js').YType<any>} */ (item.parent)
p._searchMarker = null
p._hasFormatting = true
}

View File

@@ -1,33 +1,9 @@
import {
readYArray,
readYMap,
readYText,
readYXmlElement,
readYXmlFragment,
readYXmlHook,
readYXmlText,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item // eslint-disable-line
} from '../internals.js'
/**
* @typedef {import('../utils/types.js').YType} YType_CT
*/
import * as error from 'lib0/error'
/**
* @type {Array<(decoder: UpdateDecoderV1 | UpdateDecoderV2)=>(import('../utils/types.js').YType)>}
* @private
*/
export const typeRefs = [
readYArray,
readYMap,
readYText,
readYXmlElement,
readYXmlFragment,
readYXmlHook,
readYXmlText
]
import { readYType } from '../ytype.js'
export const YArrayRefID = 0
export const YMapRefID = 1
@@ -42,11 +18,11 @@ export const YXmlTextRefID = 6
*/
export class ContentType {
/**
* @param {YType_CT} type
* @param {import('../ytype.js').YType} type
*/
constructor (type) {
/**
* @type {YType_CT}
* @type {import('../ytype.js').YType}
*/
this.type = type
}
@@ -173,4 +149,4 @@ export class ContentType {
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {ContentType}
*/
export const readContentType = decoder => new ContentType(typeRefs[decoder.readTypeRef()](decoder))
export const readContentType = decoder => new ContentType(readYType(decoder))

View File

@@ -29,10 +29,6 @@ import * as error from 'lib0/error'
import * as binary from 'lib0/binary'
import * as array from 'lib0/array'
/**
* @typedef {import('../utils/types.js').YType} YType__
*/
/**
* @todo This should return several items
*
@@ -72,7 +68,7 @@ export const followRedone = (store, id) => {
export const keepItem = (item, keep) => {
while (item !== null && item.keep !== keep) {
item.keep = keep
item = /** @type {YType__} */ (item.parent)._item
item = /** @type {YType} */ (item.parent)._item
}
}
@@ -119,7 +115,7 @@ export const splitItem = (transaction, leftItem, diff) => {
transaction._mergeStructs.push(rightItem)
// update parent._map
if (rightItem.parentSub !== null && rightItem.right === null) {
/** @type {YType__} */ (rightItem.parent)._map.set(rightItem.parentSub, rightItem)
/** @type {YType} */ (rightItem.parent)._map.set(rightItem.parentSub, rightItem)
}
} else {
rightItem.left = null
@@ -177,7 +173,7 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo
if (redone !== null) {
return getItemCleanStart(transaction, redone)
}
let parentItem = /** @type {YType__} */ (item.parent)._item
let parentItem = /** @type {YType} */ (item.parent)._item
/**
* @type {Item|null}
*/
@@ -197,9 +193,9 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo
}
}
/**
* @type {YType__}
* @type {YType}
*/
const parentType = /** @type {YType__} */ (parentItem === null ? item.parent : /** @type {ContentType} */ (parentItem.content).type)
const parentType = /** @type {YType} */ (parentItem === null ? item.parent : /** @type {ContentType} */ (parentItem.content).type)
if (item.parentSub === null) {
// Is an array item. Insert at the old position
@@ -212,10 +208,10 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo
*/
let leftTrace = left
// trace redone until parent matches
while (leftTrace !== null && /** @type {YType__} */ (leftTrace.parent)._item !== parentItem) {
while (leftTrace !== null && /** @type {YType} */ (leftTrace.parent)._item !== parentItem) {
leftTrace = leftTrace.redone === null ? null : getItemCleanStart(transaction, leftTrace.redone)
}
if (leftTrace !== null && /** @type {YType__} */ (leftTrace.parent)._item === parentItem) {
if (leftTrace !== null && /** @type {YType} */ (leftTrace.parent)._item === parentItem) {
left = leftTrace
break
}
@@ -227,10 +223,10 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo
*/
let rightTrace = right
// trace redone until parent matches
while (rightTrace !== null && /** @type {YType__} */ (rightTrace.parent)._item !== parentItem) {
while (rightTrace !== null && /** @type {YType} */ (rightTrace.parent)._item !== parentItem) {
rightTrace = rightTrace.redone === null ? null : getItemCleanStart(transaction, rightTrace.redone)
}
if (rightTrace !== null && /** @type {YType__} */ (rightTrace.parent)._item === parentItem) {
if (rightTrace !== null && /** @type {YType} */ (rightTrace.parent)._item === parentItem) {
right = rightTrace
break
}
@@ -282,7 +278,7 @@ export class Item extends AbstractStruct {
* @param {ID | null} origin
* @param {Item | null} right
* @param {ID | null} rightOrigin
* @param {YType<any,any>|ID|null} parent Is a type if integrated, is null if it is possible to copy parent from left or right, is ID before integration to search for it.
* @param {YType|ID|null} parent Is a type if integrated, is null if it is possible to copy parent from left or right, is ID before integration to search for it.
* @param {string | null} parentSub
* @param {AbstractContent} content
*/
@@ -309,7 +305,7 @@ export class Item extends AbstractStruct {
*/
this.rightOrigin = rightOrigin
/**
* @type {YType<any,any>|ID|null}
* @type {YType|ID|null}
*/
this.parent = parent
/**
@@ -466,12 +462,12 @@ export class Item extends AbstractStruct {
if (left !== null) {
o = left.right
} else if (this.parentSub !== null) {
o = /** @type {AbstractType<any>} */ (this.parent)._map.get(this.parentSub) || null
o = /** @type {YType} */ (this.parent)._map.get(this.parentSub) || null
while (o !== null && o.left !== null) {
o = o.left
}
} else {
o = /** @type {AbstractType<any>} */ (this.parent)._start
o = /** @type {YType} */ (this.parent)._start
}
// TODO: use something like DeleteSet here (a tree implementation would be best)
// @todo use global set definitions
@@ -520,13 +516,13 @@ export class Item extends AbstractStruct {
} else {
let r
if (this.parentSub !== null) {
r = /** @type {AbstractType<any>} */ (this.parent)._map.get(this.parentSub) || null
r = /** @type {YType} */ (this.parent)._map.get(this.parentSub) || null
while (r !== null && r.left !== null) {
r = r.left
}
} else {
r = /** @type {AbstractType<any>} */ (this.parent)._start
;/** @type {AbstractType<any>} */ (this.parent)._start = this
r = /** @type {YType} */ (this.parent)._start
;/** @type {YType} */ (this.parent)._start = this
}
this.right = r
}
@@ -534,7 +530,7 @@ export class Item extends AbstractStruct {
this.right.left = this
} else if (this.parentSub !== null) {
// set as current parent value if right === null and this is parentSub
/** @type {AbstractType<any>} */ (this.parent)._map.set(this.parentSub, this)
/** @type {YType} */ (this.parent)._map.set(this.parentSub, this)
if (this.left !== null) {
// this is the current attribute value of parent. delete right
this.left.delete(transaction)
@@ -542,14 +538,14 @@ export class Item extends AbstractStruct {
}
// adjust length of parent
if (this.parentSub === null && this.countable && !this.deleted) {
/** @type {AbstractType<any>} */ (this.parent)._length += this.length
/** @type {YType} */ (this.parent)._length += this.length
}
addStructToIdSet(transaction.insertSet, this)
addStruct(transaction.doc.store, this)
this.content.integrate(transaction, this)
// add parent to transaction.changed
addChangedTypeToTransaction(transaction, /** @type {import('../utils/types.js').YType} */ (this.parent), this.parentSub)
if ((/** @type {AbstractType<any>} */ (this.parent)._item !== null && /** @type {AbstractType<any>} */ (this.parent)._item.deleted) || (this.parentSub !== null && this.right !== null)) {
addChangedTypeToTransaction(transaction, /** @type {YType} */ (this.parent), this.parentSub)
if ((/** @type {YType} */ (this.parent)._item !== null && /** @type {YType} */ (this.parent)._item.deleted) || (this.parentSub !== null && this.right !== null)) {
// delete if parent is deleted or if this is not the current attribute value of parent
this.delete(transaction)
}
@@ -609,7 +605,7 @@ export class Item extends AbstractStruct {
this.content.constructor === right.content.constructor &&
this.content.mergeWith(right.content)
) {
const searchMarker = /** @type {AbstractType<any>} */ (this.parent)._searchMarker
const searchMarker = /** @type {YType} */ (this.parent)._searchMarker
if (searchMarker) {
searchMarker.forEach(marker => {
if (marker.p === right) {
@@ -642,7 +638,7 @@ export class Item extends AbstractStruct {
*/
delete (transaction) {
if (!this.deleted) {
const parent = /** @type {import('../utils/types.js').YType} */ (this.parent)
const parent = /** @type {YType} */ (this.parent)
// adjust the length of parent
if (this.countable && this.parentSub === null) {
parent._length -= this.length

File diff suppressed because it is too large Load Diff

View File

@@ -1,270 +0,0 @@
/**
* @module YArray
*/
import {
AbstractType,
typeListGet,
typeListToArray,
typeListForEach,
typeListCreateIterator,
typeListInsertGenerics,
typeListPushGenerics,
typeListDelete,
typeListMap,
YArrayRefID,
transact,
warnPrematureAccess,
typeListSlice,
noAttributionsManager,
AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
} from '../internals.js'
import * as delta from 'lib0/delta'
/**
* A shared Array implementation.
* @template {import('../utils/types.js').YValue} T
* @extends {AbstractType<delta.Delta<T>,YArray<T>>}
* @implements {Iterable<T>}
*/
// @todo remove this
// @ts-ignore
export class YType extends AbstractType {
constructor () {
super()
/**
* @type {Array<any>?}
* @private
*/
this._prelimContent = []
/**
* @type {Array<ArraySearchMarker>}
*/
this._searchMarker = []
}
/**
* Construct a new YArray containing the specified items.
* @template {import('../utils/types.js').YValue} T
* @param {Array<T>} items
* @return {YArray<T>}
*/
static from (items) {
/**
* @type {YArray<T>}
*/
const a = new YArray()
a.push(items)
return a
}
/**
* Integrate this type into the Yjs instance.
*
* * Save this struct in the os
* * This type is sent to other client
* * Observer functions are fired
*
* @param {Doc} y The Yjs instance
* @param {Item?} item
*/
_integrate (y, item) {
super._integrate(y, item)
this.insert(0, /** @type {Array<any>} */ (this._prelimContent))
this._prelimContent = null
}
/**
* Makes a copy of this data type that can be included somewhere else.
*
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
*
* @return {YArray<T>}
*/
clone () {
/**
* @type {this}
*/
const arr = /** @type {this} */ (new YArray())
arr.insert(0, this.toArray().map(el =>
// @ts-ignore
el instanceof AbstractType ? /** @type {any} */ (el.clone()) : el
))
return arr
}
get length () {
this.doc ?? warnPrematureAccess()
return this._length
}
/**
* Inserts new content at an index.
*
* Important: This function expects an array of content. Not just a content
* object. The reason for this "weirdness" is that inserting several elements
* is very efficient when it is done as a single operation.
*
* @example
* // Insert character 'a' at position 0
* yarray.insert(0, ['a'])
* // Insert numbers 1, 2 at position 1
* yarray.insert(1, [1, 2])
*
* @param {number} index The index to insert content at.
* @param {Array<T>} content The array of content
*/
insert (index, content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListInsertGenerics(transaction, this, index, /** @type {any} */ (content))
})
} else {
/** @type {Array<any>} */ (this._prelimContent).splice(index, 0, ...content)
}
}
/**
* Appends content to this YArray.
*
* @param {Array<T>} content Array of content to append.
*
* @todo Use the following implementation in all types.
*/
push (content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListPushGenerics(transaction, this, /** @type {any} */ (content))
})
} else {
/** @type {Array<any>} */ (this._prelimContent).push(...content)
}
}
/**
* Prepends content to this YArray.
*
* @param {Array<T>} content Array of content to prepend.
*/
unshift (content) {
this.insert(0, content)
}
/**
* Deletes elements starting from an index.
*
* @param {number} index Index at which to start deleting elements
* @param {number} length The number of elements to remove. Defaults to 1.
*/
delete (index, length = 1) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListDelete(transaction, this, index, length)
})
} else {
/** @type {Array<any>} */ (this._prelimContent).splice(index, length)
}
}
/**
* Returns the i-th element from a YArray.
*
* @param {number} index The index of the element to return from the YArray
* @return {T}
*/
get (index) {
return typeListGet(this, index)
}
/**
* Transforms this YArray to a JavaScript Array.
*
* @return {Array<T>}
*/
toArray () {
return typeListToArray(this)
}
/**
* Render the difference to another ydoc (which can be empty) and highlight the differences with
* attributions.
*
* Note that deleted content that was not deleted in prevYdoc is rendered as an insertion with the
* attribution `{ isDeleted: true, .. }`.
*
* @param {AbstractAttributionManager} am
* @return {delta.ArrayDelta<import('./AbstractType.js').TypeToDelta<T>>} The Delta representation of this type.
*
* @public
*/
getContentDeep (am = noAttributionsManager) {
return super.getContentDeep(am)
}
/**
* Returns a portion of this YArray into a JavaScript Array selected
* from start to end (end not included).
*
* @param {number} [start]
* @param {number} [end]
* @return {Array<T>}
*/
slice (start = 0, end = this.length) {
return typeListSlice(this, start, end)
}
/**
* Transforms this Shared Type to a JSON object.
*
* @return {Array<any>}
*/
toJSON () {
return this.map(c => c instanceof AbstractType ? c.toJSON() : c)
}
/**
* Returns an Array with the result of calling a provided function on every
* element of this YArray.
*
* @template M
* @param {function(T,number,YArray<T>):M} f Function that produces an element of the new Array
* @return {Array<M>} A new array with each element being the result of the
* callback function
*/
map (f) {
return typeListMap(this, /** @type {any} */ (f))
}
/**
* Executes a provided function once on every element of this YArray.
*
* @param {function(T,number,YArray<T>):void} f A function to execute on every element of this YArray.
*/
forEach (f) {
typeListForEach(this, f)
}
/**
* @return {IterableIterator<T>}
*/
[Symbol.iterator] () {
return typeListCreateIterator(this)
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
*/
_write (encoder) {
encoder.writeTypeRef(YArrayRefID)
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
* @return {import('../utils/types.js').YType}
*
* @private
* @function
*/
export const readYArray = _decoder => new YArray()

View File

@@ -1,270 +0,0 @@
/**
* @module YArray
*/
import {
AbstractType,
typeListGet,
typeListToArray,
typeListForEach,
typeListCreateIterator,
typeListInsertGenerics,
typeListPushGenerics,
typeListDelete,
typeListMap,
YArrayRefID,
transact,
warnPrematureAccess,
typeListSlice,
noAttributionsManager,
AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
} from '../internals.js'
import * as delta from 'lib0/delta' // eslint-disable-line
/**
* A shared Array implementation.
* @template {import('../utils/types.js').YValue} T
* @extends {AbstractType<delta.ArrayDelta<T>,YArray<T>>}
* @implements {Iterable<T>}
*/
// @todo remove this
// @ts-ignore
export class YArray extends AbstractType {
constructor () {
super()
/**
* @type {Array<any>?}
* @private
*/
this._prelimContent = []
/**
* @type {Array<ArraySearchMarker>}
*/
this._searchMarker = []
}
/**
* Construct a new YArray containing the specified items.
* @template {import('../utils/types.js').YValue} T
* @param {Array<T>} items
* @return {YArray<T>}
*/
static from (items) {
/**
* @type {YArray<T>}
*/
const a = new YArray()
a.push(items)
return a
}
/**
* Integrate this type into the Yjs instance.
*
* * Save this struct in the os
* * This type is sent to other client
* * Observer functions are fired
*
* @param {Doc} y The Yjs instance
* @param {Item?} item
*/
_integrate (y, item) {
super._integrate(y, item)
this.insert(0, /** @type {Array<any>} */ (this._prelimContent))
this._prelimContent = null
}
/**
* Makes a copy of this data type that can be included somewhere else.
*
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
*
* @return {YArray<T>}
*/
clone () {
/**
* @type {this}
*/
const arr = /** @type {this} */ (new YArray())
arr.insert(0, this.toArray().map(el =>
// @ts-ignore
el instanceof AbstractType ? /** @type {any} */ (el.clone()) : el
))
return arr
}
get length () {
this.doc ?? warnPrematureAccess()
return this._length
}
/**
* Inserts new content at an index.
*
* Important: This function expects an array of content. Not just a content
* object. The reason for this "weirdness" is that inserting several elements
* is very efficient when it is done as a single operation.
*
* @example
* // Insert character 'a' at position 0
* yarray.insert(0, ['a'])
* // Insert numbers 1, 2 at position 1
* yarray.insert(1, [1, 2])
*
* @param {number} index The index to insert content at.
* @param {Array<T>} content The array of content
*/
insert (index, content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListInsertGenerics(transaction, this, index, /** @type {any} */ (content))
})
} else {
/** @type {Array<any>} */ (this._prelimContent).splice(index, 0, ...content)
}
}
/**
* Appends content to this YArray.
*
* @param {Array<T>} content Array of content to append.
*
* @todo Use the following implementation in all types.
*/
push (content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListPushGenerics(transaction, this, /** @type {any} */ (content))
})
} else {
/** @type {Array<any>} */ (this._prelimContent).push(...content)
}
}
/**
* Prepends content to this YArray.
*
* @param {Array<T>} content Array of content to prepend.
*/
unshift (content) {
this.insert(0, content)
}
/**
* Deletes elements starting from an index.
*
* @param {number} index Index at which to start deleting elements
* @param {number} length The number of elements to remove. Defaults to 1.
*/
delete (index, length = 1) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListDelete(transaction, this, index, length)
})
} else {
/** @type {Array<any>} */ (this._prelimContent).splice(index, length)
}
}
/**
* Returns the i-th element from a YArray.
*
* @param {number} index The index of the element to return from the YArray
* @return {T}
*/
get (index) {
return typeListGet(this, index)
}
/**
* Transforms this YArray to a JavaScript Array.
*
* @return {Array<T>}
*/
toArray () {
return typeListToArray(this)
}
/**
* Render the difference to another ydoc (which can be empty) and highlight the differences with
* attributions.
*
* Note that deleted content that was not deleted in prevYdoc is rendered as an insertion with the
* attribution `{ isDeleted: true, .. }`.
*
* @param {AbstractAttributionManager} am
* @return {delta.ArrayDelta<import('./AbstractType.js').TypeToDelta<T>>} The Delta representation of this type.
*
* @public
*/
getContentDeep (am = noAttributionsManager) {
return super.getContentDeep(am)
}
/**
* Returns a portion of this YArray into a JavaScript Array selected
* from start to end (end not included).
*
* @param {number} [start]
* @param {number} [end]
* @return {Array<T>}
*/
slice (start = 0, end = this.length) {
return typeListSlice(this, start, end)
}
/**
* Transforms this Shared Type to a JSON object.
*
* @return {Array<any>}
*/
toJSON () {
return this.map(c => c instanceof AbstractType ? c.toJSON() : c)
}
/**
* Returns an Array with the result of calling a provided function on every
* element of this YArray.
*
* @template M
* @param {function(T,number,YArray<T>):M} f Function that produces an element of the new Array
* @return {Array<M>} A new array with each element being the result of the
* callback function
*/
map (f) {
return typeListMap(this, /** @type {any} */ (f))
}
/**
* Executes a provided function once on every element of this YArray.
*
* @param {function(T,number,YArray<T>):void} f A function to execute on every element of this YArray.
*/
forEach (f) {
typeListForEach(this, f)
}
/**
* @return {IterableIterator<T>}
*/
[Symbol.iterator] () {
return typeListCreateIterator(this)
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
*/
_write (encoder) {
encoder.writeTypeRef(YArrayRefID)
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
* @return {import('../utils/types.js').YType}
*
* @private
* @function
*/
export const readYArray = _decoder => new YArray()

View File

@@ -1,244 +0,0 @@
/**
* @module YMap
*/
import {
AbstractType,
typeMapDelete,
typeMapSet,
typeMapGet,
typeMapHas,
createMapIterator,
YMapRefID,
transact,
warnPrematureAccess,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item // eslint-disable-line
} from '../internals.js'
import * as iterator from 'lib0/iterator'
import * as delta from 'lib0/delta' // eslint-disable-line
/**
* @template MapType
* A shared Map implementation.
*
* @extends AbstractType<delta.MapDelta<{[K in string]:MapType}>>
* @implements {Iterable<[string, MapType]>}
*/
export class YMap extends AbstractType {
/**
*
* @param {Iterable<readonly [string, any]>=} entries - an optional iterable to initialize the YMap
*/
constructor (entries) {
super()
/**
* @type {Map<string,any>?}
* @private
*/
this._prelimContent = null
if (entries === undefined) {
this._prelimContent = new Map()
} else {
this._prelimContent = new Map(entries)
}
}
/**
* Integrate this type into the Yjs instance.
*
* * Save this struct in the os
* * This type is sent to other client
* * Observer functions are fired
*
* @param {Doc} y The Yjs instance
* @param {Item?} item
*/
_integrate (y, item) {
super._integrate(y, item)
;/** @type {Map<string, any>} */ (this._prelimContent).forEach((value, key) => {
this.set(key, value)
})
this._prelimContent = null
}
/**
* Makes a copy of this data type that can be included somewhere else.
*
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
*
* @return {this}
*/
clone () {
const map = this._copy()
this.forEach((value, key) => {
map.set(key, value instanceof AbstractType ? /** @type {typeof value} */ (value.clone()) : value)
})
return map
}
/**
* Transforms this Shared Type to a JSON object.
*
* @return {Object<string,any>}
*/
toJSON () {
this.doc ?? warnPrematureAccess()
/**
* @type {Object<string,MapType>}
*/
const map = {}
this._map.forEach((item, key) => {
if (!item.deleted) {
const v = item.content.getContent()[item.length - 1]
map[key] = v instanceof AbstractType ? v.toJSON() : v
}
})
return map
}
/**
* Returns the size of the YMap (count of key/value pairs)
*
* @return {number}
*/
get size () {
return [...createMapIterator(this)].length
}
/**
* Returns the keys for each element in the YMap Type.
*
* @return {IterableIterator<string>}
*/
keys () {
return iterator.iteratorMap(createMapIterator(this), /** @param {any} v */ v => v[0])
}
/**
* Returns the values for each element in the YMap Type.
*
* @return {IterableIterator<MapType>}
*/
values () {
return iterator.iteratorMap(createMapIterator(this), /** @param {any} v */ v => v[1].content.getContent()[v[1].length - 1])
}
/**
* Returns an Iterator of [key, value] pairs
*
* @return {IterableIterator<[string, MapType]>}
*/
entries () {
return iterator.iteratorMap(createMapIterator(this), /** @param {any} v */ v => /** @type {any} */ ([v[0], v[1].content.getContent()[v[1].length - 1]]))
}
/**
* Executes a provided function on once on every key-value pair.
*
* @param {function(MapType,string,YMap<MapType>):void} f A function to execute on every element of this YArray.
*/
forEach (f) {
this.doc ?? warnPrematureAccess()
this._map.forEach((item, key) => {
if (!item.deleted) {
f(item.content.getContent()[item.length - 1], key, this)
}
})
}
/**
* Returns an Iterator of [key, value] pairs
*
* @return {IterableIterator<[string, MapType]>}
*/
[Symbol.iterator] () {
return this.entries()
}
/**
* Remove a specified element from this YMap.
*
* @param {string} key The key of the element to remove.
*/
delete (key) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeMapDelete(transaction, this, key)
})
} else {
/** @type {Map<string, any>} */ (this._prelimContent).delete(key)
}
}
/**
* Adds or updates an element with a specified key and value.
* @template {MapType} VAL
*
* @param {string} key The key of the element to add to this YMap
* @param {VAL} value The value of the element to add
* @return {VAL}
*/
set (key, value) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeMapSet(transaction, this, key, /** @type {any} */ (value))
})
} else {
/** @type {Map<string, any>} */ (this._prelimContent).set(key, value)
}
return value
}
/**
* Returns a specified element from this YMap.
*
* @param {string} key
* @return {MapType|undefined}
*/
get (key) {
return /** @type {any} */ (typeMapGet(this, key))
}
/**
* Returns a boolean indicating whether the specified key exists or not.
*
* @param {string} key The key to test.
* @return {boolean}
*/
has (key) {
return typeMapHas(this, key)
}
/**
* Removes all elements from this YMap.
*/
clear () {
if (this.doc !== null) {
transact(this.doc, transaction => {
this.forEach(function (_value, key, map) {
typeMapDelete(transaction, map, key)
})
})
} else {
/** @type {Map<string, any>} */ (this._prelimContent).clear()
}
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
*/
_write (encoder) {
encoder.writeTypeRef(YMapRefID)
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
* @return {import('../utils/types.js').YType}
*
* @private
* @function
*/
export const readYMap = _decoder => new YMap()

View File

@@ -1,934 +0,0 @@
/**
* @module YText
*/
import {
AbstractType,
getItemCleanStart,
getState,
createID,
YTextRefID,
transact,
ContentEmbed,
GC,
ContentFormat,
ContentString,
iterateStructsByIdSet,
findMarker,
typeMapDelete,
typeMapSet,
typeMapGet,
typeMapGetAll,
updateMarkerChanges,
ContentType,
warnPrematureAccess,
noAttributionsManager, AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item, Transaction, // eslint-disable-line
createIdSet,
equalAttrs
} from '../internals.js'
import * as math from 'lib0/math'
import * as traits from 'lib0/traits'
import * as map from 'lib0/map'
import * as error from 'lib0/error'
export class ItemTextListPosition {
/**
* @param {Item|null} left
* @param {Item|null} right
* @param {number} index
* @param {Map<string,any>} currentAttributes
* @param {AbstractAttributionManager} am
*/
constructor (left, right, index, currentAttributes, am) {
this.left = left
this.right = right
this.index = index
this.currentAttributes = currentAttributes
this.am = am
}
/**
* Only call this if you know that this.right is defined
*/
forward () {
if (this.right === null) {
error.unexpectedCase()
}
switch (this.right.content.constructor) {
case ContentFormat:
if (!this.right.deleted) {
updateCurrentAttributes(this.currentAttributes, /** @type {ContentFormat} */ (this.right.content))
}
break
default:
this.index += this.am.contentLength(this.right)
break
}
this.left = this.right
this.right = this.right.right
}
/**
* @param {Transaction} transaction
* @param {import('../utils/types.js').YType} parent
* @param {number} length
* @param {Object<string,any>} attributes
*
* @function
*/
formatText (transaction, parent, length, attributes) {
const doc = transaction.doc
const ownClientId = doc.clientID
minimizeAttributeChanges(this, attributes)
const negatedAttributes = insertAttributes(transaction, parent, this, attributes)
// iterate until first non-format or null is found
// delete all formats with attributes[format.key] != null
// also check the attributes after the first non-format as we do not want to insert redundant negated attributes there
// eslint-disable-next-line no-labels
iterationLoop: while (
this.right !== null &&
(length > 0 ||
(
negatedAttributes.size > 0 &&
((this.right.deleted && this.am.contentLength(this.right) === 0) || this.right.content.constructor === ContentFormat)
)
)
) {
switch (this.right.content.constructor) {
case ContentFormat: {
if (!this.right.deleted) {
const { key, value } = /** @type {ContentFormat} */ (this.right.content)
const attr = attributes[key]
if (attr !== undefined) {
if (equalAttrs(attr, value)) {
negatedAttributes.delete(key)
} else {
if (length === 0) {
// no need to further extend negatedAttributes
// eslint-disable-next-line no-labels
break iterationLoop
}
negatedAttributes.set(key, value)
}
this.right.delete(transaction)
} else {
this.currentAttributes.set(key, value)
}
}
break
}
default: {
const item = this.right
const rightLen = this.am.contentLength(item)
if (length < rightLen) {
/**
* @type {Array<import('../internals.js').AttributedContent<any>>}
*/
const contents = []
this.am.readContent(contents, item.id.client, item.id.clock, item.deleted, item.content, 0)
let i = 0
for (; i < contents.length && length > 0; i++) {
const c = contents[i]
if ((!c.deleted || c.attrs != null) && c.content.isCountable()) {
length -= c.content.getLength()
}
}
if (length < 0 || (length === 0 && i !== contents.length)) {
const c = contents[--i]
getItemCleanStart(transaction, createID(item.id.client, c.clock + c.content.getLength() + length))
}
} else {
length -= rightLen
}
break
}
}
this.forward()
}
// Quill just assumes that the editor starts with a newline and that it always
// ends with a newline. We only insert that newline when a new newline is
// inserted - i.e when length is bigger than type.length
if (length > 0) {
let newlines = ''
for (; length > 0; length--) {
newlines += '\n'
}
this.right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), this.left, this.left && this.left.lastId, this.right, this.right && this.right.id, parent, null, new ContentString(newlines))
this.right.integrate(transaction, 0)
this.forward()
}
insertNegatedAttributes(transaction, parent, this, negatedAttributes)
}
}
/**
* @param {Transaction} transaction
* @param {ItemTextListPosition} pos
* @param {number} count steps to move forward
* @return {ItemTextListPosition}
*
* @private
* @function
*/
const findNextPosition = (transaction, pos, count) => {
while (pos.right !== null && count > 0) {
switch (pos.right.content.constructor) {
case ContentFormat:
if (!pos.right.deleted) {
updateCurrentAttributes(pos.currentAttributes, /** @type {ContentFormat} */ (pos.right.content))
}
break
default:
if (!pos.right.deleted) {
if (count < pos.right.length) {
// split right
getItemCleanStart(transaction, createID(pos.right.id.client, pos.right.id.clock + count))
}
pos.index += pos.right.length
count -= pos.right.length
}
break
}
pos.left = pos.right
pos.right = pos.right.right
// pos.forward() - we don't forward because that would halve the performance because we already do the checks above
}
return pos
}
/**
* @param {Transaction} transaction
* @param {import('../utils/types.js').YType} parent
* @param {number} index
* @param {boolean} useSearchMarker
* @return {ItemTextListPosition}
*
* @private
* @function
*/
const findPosition = (transaction, parent, index, useSearchMarker) => {
const currentAttributes = new Map()
const marker = useSearchMarker ? findMarker(parent, index) : null
if (marker) {
const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes, noAttributionsManager)
return findNextPosition(transaction, pos, index - marker.index)
} else {
const pos = new ItemTextListPosition(null, parent._start, 0, currentAttributes, noAttributionsManager)
return findNextPosition(transaction, pos, index)
}
}
/**
* Negate applied formats
*
* @param {Transaction} transaction
* @param {import('../utils/types.js').YType} parent
* @param {ItemTextListPosition} currPos
* @param {Map<string,any>} negatedAttributes
*
* @private
* @function
*/
const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes) => {
// check if we really need to remove attributes
while (
currPos.right !== null && (
(currPos.right.deleted && (currPos.am === noAttributionsManager || currPos.am.contentLength(currPos.right) === 0)) || (
currPos.right.content.constructor === ContentFormat &&
equalAttrs(negatedAttributes.get(/** @type {ContentFormat} */ (currPos.right.content).key), /** @type {ContentFormat} */ (currPos.right.content).value)
)
)
) {
if (!currPos.right.deleted) {
negatedAttributes.delete(/** @type {ContentFormat} */ (currPos.right.content).key)
}
currPos.forward()
}
const doc = transaction.doc
const ownClientId = doc.clientID
negatedAttributes.forEach((val, key) => {
const left = currPos.left
const right = currPos.right
const nextFormat = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
nextFormat.integrate(transaction, 0)
currPos.right = nextFormat
currPos.forward()
})
}
/**
* @param {Map<string,any>} currentAttributes
* @param {ContentFormat} format
*
* @private
* @function
*/
const updateCurrentAttributes = (currentAttributes, format) => {
const { key, value } = format
if (value === null) {
currentAttributes.delete(key)
} else {
currentAttributes.set(key, value)
}
}
/**
* @param {ItemTextListPosition} currPos
* @param {Object<string,any>} attributes
*
* @private
* @function
*/
const minimizeAttributeChanges = (currPos, attributes) => {
// go right while attributes[right.key] === right.value (or right is deleted)
while (true) {
if (currPos.right === null) {
break
} else if (currPos.right.deleted ? (currPos.am.contentLength(currPos.right) === 0) : (!currPos.right.deleted && currPos.right.content.constructor === ContentFormat && equalAttrs(attributes[(/** @type {ContentFormat} */ (currPos.right.content)).key] ?? null, /** @type {ContentFormat} */ (currPos.right.content).value))) {
//
} else {
break
}
currPos.forward()
}
}
/**
* @param {Transaction} transaction
* @param {import('../utils/types.js').YType} parent
* @param {ItemTextListPosition} currPos
* @param {Object<string,any>} attributes
* @return {Map<string,any>}
*
* @private
* @function
**/
const insertAttributes = (transaction, parent, currPos, attributes) => {
const doc = transaction.doc
const ownClientId = doc.clientID
const negatedAttributes = new Map()
// insert format-start items
for (const key in attributes) {
const val = attributes[key]
const currentVal = currPos.currentAttributes.get(key) ?? null
if (!equalAttrs(currentVal, val)) {
// save negated attribute (set null if currentVal undefined)
negatedAttributes.set(key, currentVal)
const { left, right } = currPos
currPos.right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val))
currPos.right.integrate(transaction, 0)
currPos.forward()
}
}
return negatedAttributes
}
/**
* @param {Transaction} transaction
* @param {import('../utils/types.js').YType} parent
* @param {ItemTextListPosition} currPos
* @param {string|object|import('../utils/types.js').YType} text
* @param {Object<string,any>} attributes
*
* @private
* @function
**/
export const insertText = (transaction, parent, currPos, text, attributes) => {
currPos.currentAttributes.forEach((_val, key) => {
if (attributes[key] === undefined) {
attributes[key] = null
}
})
const doc = transaction.doc
const ownClientId = doc.clientID
minimizeAttributeChanges(currPos, attributes)
const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes)
// insert content
const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : (text instanceof AbstractType ? new ContentType(text) : new ContentEmbed(text))
let { left, right, index } = currPos
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength())
}
right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content)
right.integrate(transaction, 0)
currPos.right = right
currPos.index = index
currPos.forward()
insertNegatedAttributes(transaction, parent, currPos, negatedAttributes)
}
/**
* Call this function after string content has been deleted in order to
* clean up formatting Items.
*
* @param {Transaction} transaction
* @param {Item} start
* @param {Item|null} curr exclusive end, automatically iterates to the next Content Item
* @param {Map<string,any>} startAttributes
* @param {Map<string,any>} currAttributes
* @return {number} The amount of formatting Items deleted.
*
* @function
*/
const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => {
if (!transaction.doc.cleanupFormatting) return 0
/**
* @type {Item|null}
*/
let end = start
/**
* @type {Map<string,ContentFormat>}
*/
const endFormats = map.create()
while (end && (!end.countable || end.deleted)) {
if (!end.deleted && end.content.constructor === ContentFormat) {
const cf = /** @type {ContentFormat} */ (end.content)
endFormats.set(cf.key, cf)
}
end = end.right
}
let cleanups = 0
let reachedCurr = false
while (start !== end) {
if (curr === start) {
reachedCurr = true
}
if (!start.deleted) {
const content = start.content
switch (content.constructor) {
case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (content)
const startAttrValue = startAttributes.get(key) ?? null
if (endFormats.get(key) !== content || startAttrValue === value) {
// Either this format is overwritten or it is not necessary because the attribute already existed.
start.delete(transaction)
transaction.cleanUps.add(start.id.client, start.id.clock, start.length)
cleanups++
if (!reachedCurr && (currAttributes.get(key) ?? null) === value && startAttrValue !== value) {
if (startAttrValue === null) {
currAttributes.delete(key)
} else {
currAttributes.set(key, startAttrValue)
}
}
}
if (!reachedCurr && !start.deleted) {
updateCurrentAttributes(currAttributes, /** @type {ContentFormat} */ (content))
}
break
}
}
}
start = /** @type {Item} */ (start.right)
}
return cleanups
}
/**
* @param {Transaction} transaction
* @param {Item | null} item
*/
const cleanupContextlessFormattingGap = (transaction, item) => {
if (!transaction.doc.cleanupFormatting) return 0
// iterate until item.right is null or content
while (item && item.right && (item.right.deleted || !item.right.countable)) {
item = item.right
}
const attrs = new Set()
// iterate back until a content item is found
while (item && (item.deleted || !item.countable)) {
if (!item.deleted && item.content.constructor === ContentFormat) {
const key = /** @type {ContentFormat} */ (item.content).key
if (attrs.has(key)) {
item.delete(transaction)
transaction.cleanUps.add(item.id.client, item.id.clock, item.length)
} else {
attrs.add(key)
}
}
item = item.left
}
}
/**
* This function is experimental and subject to change / be removed.
*
* Ideally, we don't need this function at all. Formatting attributes should be cleaned up
* automatically after each change. This function iterates twice over the complete YText type
* and removes unnecessary formatting attributes. This is also helpful for testing.
*
* This function won't be exported anymore as soon as there is confidence that the YText type works as intended.
*
* @param {YText<any>} type
* @return {number} How many formatting attributes have been cleaned up.
*/
export const cleanupYTextFormatting = type => {
if (!type.doc?.cleanupFormatting) return 0
let res = 0
transact(/** @type {Doc} */ (type.doc), transaction => {
let start = /** @type {Item} */ (type._start)
let end = type._start
let startAttributes = map.create()
const currentAttributes = map.copy(startAttributes)
while (end) {
if (end.deleted === false) {
switch (end.content.constructor) {
case ContentFormat:
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (end.content))
break
default:
res += cleanupFormattingGap(transaction, start, end, startAttributes, currentAttributes)
startAttributes = map.copy(currentAttributes)
start = end
break
}
}
end = end.right
}
})
return res
}
/**
* This will be called by the transaction once the event handlers are called to potentially cleanup
* formatting attributes.
*
* @param {Transaction} transaction
*/
export const cleanupYTextAfterTransaction = transaction => {
/**
* @type {Set<YText<any>>}
*/
const needFullCleanup = new Set()
// check if another formatting item was inserted
const doc = transaction.doc
iterateStructsByIdSet(transaction, transaction.insertSet, (item) => {
if (
!item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat && item.constructor !== GC
) {
needFullCleanup.add(/** @type {any} */ (item).parent)
}
})
// cleanup in a new transaction
transact(doc, (t) => {
iterateStructsByIdSet(transaction, transaction.deleteSet, item => {
if (item instanceof GC || !(/** @type {YText<any>} */ (item.parent)._hasFormatting) || needFullCleanup.has(/** @type {YText<any>} */ (item.parent))) {
return
}
const parent = /** @type {YText<any>} */ (item.parent)
if (item.content.constructor === ContentFormat) {
needFullCleanup.add(parent)
} else {
// If no formatting attribute was inserted or deleted, we can make due with contextless
// formatting cleanups.
// Contextless: it is not necessary to compute currentAttributes for the affected position.
cleanupContextlessFormattingGap(t, item)
}
})
// If a formatting item was inserted, we simply clean the whole type.
// We need to compute currentAttributes for the current position anyway.
for (const yText of needFullCleanup) {
cleanupYTextFormatting(yText)
}
})
}
/**
* @param {Transaction} transaction
* @param {ItemTextListPosition} currPos
* @param {number} length
* @return {ItemTextListPosition}
*
* @private
* @function
*/
export const deleteText = (transaction, currPos, length) => {
const startLength = length
const startAttrs = map.copy(currPos.currentAttributes)
const start = currPos.right
while (length > 0 && currPos.right !== null) {
if (!currPos.right.deleted) {
switch (currPos.right.content.constructor) {
case ContentType:
case ContentEmbed:
case ContentString:
if (length < currPos.right.length) {
getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length))
}
length -= currPos.right.length
currPos.right.delete(transaction)
break
}
} else if (currPos.am !== noAttributionsManager) {
const item = currPos.right
/**
* @type {Array<import('../internals.js').AttributedContent<any>>}
*/
const contents = []
currPos.am.readContent(contents, item.id.client, item.id.clock, true, item.content, 0)
for (let i = 0; i < contents.length; i++) {
const c = contents[i]
if (c.content.isCountable() && c.attrs != null) {
// deleting already deleted content. store that information in a meta property, but do
// nothing
const contentLen = math.min(c.content.getLength(), length)
map.setIfUndefined(transaction.meta, 'attributedDeletes', createIdSet).add(item.id.client, c.clock, contentLen)
length -= contentLen
}
}
const lastContent = contents.length > 0 ? contents[contents.length - 1] : null
const nextItemClock = item.id.clock + item.length
const nextContentClock = lastContent != null ? lastContent.clock + lastContent.content.getLength() : nextItemClock
if (nextContentClock < nextItemClock) {
getItemCleanStart(transaction, createID(item.id.client, nextContentClock))
}
}
currPos.forward()
}
if (start) {
cleanupFormattingGap(transaction, start, currPos.right, startAttrs, currPos.currentAttributes)
}
const parent = /** @type {AbstractType<any>} */ (/** @type {Item} */ (currPos.left || currPos.right).parent)
if (parent._searchMarker) {
updateMarkerChanges(parent._searchMarker, currPos.index, -startLength + length)
}
return currPos
}
/**
* The Quill Delta format represents changes on a text document with
* formatting information. For more information visit {@link https://quilljs.com/docs/delta/|Quill Delta}
*
* @example
* {
* ops: [
* { insert: 'Gandalf', attributes: { bold: true } },
* { insert: ' the ' },
* { insert: 'Grey', attributes: { color: '#cccccc' } }
* ]
* }
*
*/
/**
* Attributes that can be assigned to a selection of text.
*
* @example
* {
* bold: true,
* font-size: '40px'
* }
*
* @typedef {Object} TextAttributes
*/
/**
* Type that represents text with formatting information.
*
* This type replaces y-richtext as this implementation is able to handle
* block formats (format information on a paragraph), embeds (complex elements
* like pictures and videos), and text formats (**bold**, *italic*).
*
* @template {{ [key:string]:any } | import('../utils/types.js').YType} [Embeds={ [key:string]:any } | import('../utils/types.js').YType]
* @extends {AbstractType<import('lib0/delta').TextDelta<Embeds>>}
*/
export class YText extends AbstractType {
/**
* @param {String} [string] The initial value of the YText.
*/
constructor (string) {
super()
/**
* Array of pending operations on this type
* @type {Array<function():void>?}
*/
this._pending = string !== undefined ? [() => this.insert(0, string)] : []
/**
* @type {Array<ArraySearchMarker>|null}
*/
this._searchMarker = []
/**
* Whether this YText contains formatting attributes.
* This flag is updated when a formatting item is integrated (see ContentFormat.integrate)
*/
this._hasFormatting = false
}
/**
* Number of characters of this text type.
*
* @type {number}
*/
get length () {
this.doc ?? warnPrematureAccess()
return this._length
}
/**
* @param {Doc} y
* @param {Item?} item
*/
_integrate (y, item) {
super._integrate(y, item)
try {
/** @type {Array<function>} */ (this._pending).forEach(f => f())
} catch (e) {
console.error(e)
}
this._pending = null
}
/**
* Makes a copy of this data type that can be included somewhere else.
*
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
*
* @return {YText<Embeds>}
*/
clone () {
/**
* @type {YText<Embeds>}
*/
const text = /** @type {any} */ (new YText())
text.applyDelta(this.getContent())
return text
}
/**
* Creates YTextEvent and calls observers.
*
* @param {Transaction} transaction
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*/
_callObserver (transaction, parentSubs) {
super._callObserver(transaction, parentSubs)
// If a remote change happened, we try to cleanup potential formatting duplicates.
if (!transaction.local && this._hasFormatting) {
transaction._needFormattingCleanup = true
}
}
/**
* Returns the unformatted string representation of this YText type.
*
* @public
*/
toString () {
this.doc ?? warnPrematureAccess()
let str = ''
/**
* @type {Item|null}
*/
let n = this._start
while (n !== null) {
if (!n.deleted && n.countable && n.content.constructor === ContentString) {
str += /** @type {ContentString} */ (n.content).str
}
n = n.right
}
return str
}
/**
* Returns the unformatted string representation of this YText type.
*
* @return {string}
* @public
*/
toJSON () {
return this.toString()
}
/**
* Insert text at a given index.
*
* @param {number} index The index at which to start inserting.
* @param {String} text The text to insert at the specified position.
* @param {TextAttributes} [attributes] Optionally define some formatting
* information to apply on the inserted
* Text.
* @public
*/
insert (index, text, attributes) {
if (text.length <= 0) {
return
}
const y = this.doc
if (y !== null) {
transact(y, transaction => {
const pos = findPosition(transaction, this, index, !attributes)
if (!attributes) {
attributes = {}
// @ts-ignore
pos.currentAttributes.forEach((v, k) => { attributes[k] = v })
}
insertText(transaction, this, pos, text, attributes)
})
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.insert(index, text, attributes))
}
}
/**
* Inserts an embed at a index.
*
* @param {number} index The index to insert the embed at.
* @param {Object | AbstractType<any>} embed The Object that represents the embed.
* @param {TextAttributes} [attributes] Attribute information to apply on the
* embed
*
* @public
*/
insertEmbed (index, embed, attributes) {
const y = this.doc
if (y !== null) {
transact(y, transaction => {
const pos = findPosition(transaction, this, index, !attributes)
insertText(transaction, this, pos, embed, attributes || {})
})
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.insertEmbed(index, embed, attributes || {}))
}
}
/**
* Deletes text starting from an index.
*
* @param {number} index Index at which to start deleting.
* @param {number} length The number of characters to remove. Defaults to 1.
*
* @public
*/
delete (index, length) {
if (length === 0) {
return
}
const y = this.doc
if (y !== null) {
transact(y, transaction => {
deleteText(transaction, findPosition(transaction, this, index, true), length)
})
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.delete(index, length))
}
}
/**
* Assigns properties to a range of text.
*
* @param {number} index The position where to start formatting.
* @param {number} length The amount of characters to assign properties to.
* @param {TextAttributes} attributes Attribute information to apply on the
* text.
*
* @public
*/
format (index, length, attributes) {
if (length === 0) {
return
}
const y = this.doc
if (y !== null) {
transact(y, transaction => {
const pos = findPosition(transaction, this, index, false)
if (pos.right === null) {
return
}
pos.formatText(transaction, this, length, attributes)
})
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.format(index, length, attributes))
}
}
/**
* Removes an attribute.
*
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
*
* @param {String} attributeName The attribute name that is to be removed.
*
* @public
*/
removeAttribute (attributeName) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeMapDelete(transaction, this, attributeName)
})
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.removeAttribute(attributeName))
}
}
/**
* Sets or updates an attribute.
*
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
*
* @param {String} attributeName The attribute name that is to be set.
* @param {any} attributeValue The attribute value that is to be set.
*
* @public
*/
setAttribute (attributeName, attributeValue) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeMapSet(transaction, this, attributeName, attributeValue)
})
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.setAttribute(attributeName, attributeValue))
}
}
/**
* Returns an attribute value that belongs to the attribute name.
*
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
*
* @param {String} attributeName The attribute name that identifies the
* queried value.
* @return {any} The queried attribute value.
*
* @public
*/
getAttribute (attributeName) {
return /** @type {any} */ (typeMapGet(this, attributeName))
}
/**
* Returns all attribute name/value pairs in a JSON Object.
*
* @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks.
*
* @return {Object<string, any>} A JSON Object that describes the attributes.
*
* @public
*/
getAttributes () {
return typeMapGetAll(this)
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
*/
_write (encoder) {
encoder.writeTypeRef(YTextRefID)
}
/**
* @param {this} other
*/
[traits.EqualityTraitSymbol] (other) {
return this.getContent().equals(other.getContent())
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
* @return {import('../utils/types.js').YType}
*
* @private
* @function
*/
export const readYText = _decoder => new YText()

View File

@@ -1,227 +0,0 @@
import * as object from 'lib0/object'
import {
YXmlFragment,
transact,
typeMapDelete,
typeMapHas,
typeMapSet,
typeMapGet,
typeMapGetAll,
typeMapGetAllSnapshot,
YXmlElementRefID,
Snapshot, YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item, // eslint-disable-line
} from '../internals.js'
/**
* @typedef {Object|number|null|Array<any>|string|Uint8Array|AbstractType<any>} ValueTypes
*/
/**
* An YXmlElement imitates the behavior of a
* https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element
*
* * An YXmlElement has attributes (key value pairs)
* * An YXmlElement has childElements that must inherit from YXmlElement
*
* @template {{ [key: string]: any }} [Attrs={ [key: string]: string }]
* @template {any} [Children=any]
* @extends YXmlFragment<Children,Attrs>
*/
export class YXmlElement extends YXmlFragment {
constructor (nodeName = 'UNDEFINED') {
super()
this.nodeName = nodeName
/**
* @type {Map<string, any>|null}
*/
this._prelimAttrs = new Map()
}
/**
* @type {YXmlElement|YXmlText|null}
*/
get nextSibling () {
const n = this._item ? this._item.next : null
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
}
/**
* @type {YXmlElement|YXmlText|null}
*/
get prevSibling () {
const n = this._item ? this._item.prev : null
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
}
/**
* Integrate this type into the Yjs instance.
*
* * Save this struct in the os
* * This type is sent to other client
* * Observer functions are fired
*
* @param {Doc} y The Yjs instance
* @param {Item?} item
*/
_integrate (y, item) {
super._integrate(y, item)
;(/** @type {Map<string, any>} */ (this._prelimAttrs)).forEach((value, key) => {
this.setAttribute(key, value)
})
this._prelimAttrs = null
}
/**
* Creates an Item with the same effect as this Item (without position effect)
*
* @return {this}
*/
_copy () {
return /** @type {any} */ (new YXmlElement(this.nodeName))
}
/**
* Makes a copy of this data type that can be included somewhere else.
*
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
*
* @return {this}
*/
clone () {
const el = this._copy()
const attrs = this.getAttributes()
object.forEach(attrs, (value, key) => {
if (typeof value === 'string') {
el.setAttribute(key, value)
}
})
// @ts-ignore
el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
return el
}
/**
* Returns the XML serialization of this YXmlElement.
* The attributes are ordered by attribute-name, so you can easily use this
* method to compare YXmlElements
*
* @return {string} The string representation of this type.
*
* @public
*/
toString () {
const attrs = this.getAttributes()
const stringBuilder = []
const keys = []
for (const key in attrs) {
keys.push(key)
}
keys.sort()
const keysLen = keys.length
for (let i = 0; i < keysLen; i++) {
const key = keys[i]
stringBuilder.push(key + '="' + attrs[key] + '"')
}
const nodeName = this.nodeName.toLocaleLowerCase()
const attrsString = stringBuilder.length > 0 ? ' ' + stringBuilder.join(' ') : ''
return `<${nodeName}${attrsString}>${super.toString()}</${nodeName}>`
}
/**
* Removes an attribute from this YXmlElement.
*
* @param {string} attributeName The attribute name that is to be removed.
*
* @public
*/
removeAttribute (attributeName) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeMapDelete(transaction, this, attributeName)
})
} else {
/** @type {Map<string,any>} */ (this._prelimAttrs).delete(attributeName)
}
}
/**
* Sets or updates an attribute.
*
* @template {keyof Attrs & string} KEY
*
* @param {KEY} attributeName The attribute name that is to be set.
* @param {Attrs[KEY]} attributeValue The attribute value that is to be set.
*
* @public
*/
setAttribute (attributeName, attributeValue) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeMapSet(transaction, this, attributeName, /** @type {any} */ (attributeValue))
})
} else {
/** @type {Map<string, any>} */ (this._prelimAttrs).set(attributeName, attributeValue)
}
}
/**
* Returns an attribute value that belongs to the attribute name.
*
* @template {keyof Attrs & string} KEY
*
* @param {KEY} attributeName The attribute name that identifies the
* queried value.
* @return {Attrs[KEY]|undefined} The queried attribute value.
*
* @public
*/
getAttribute (attributeName) {
return /** @type {any} */ (typeMapGet(this, attributeName))
}
/**
* Returns whether an attribute exists
*
* @param {string} attributeName The attribute name to check for existence.
* @return {boolean} whether the attribute exists.
*
* @public
*/
hasAttribute (attributeName) {
return /** @type {any} */ (typeMapHas(this, attributeName))
}
/**
* Returns all attribute name/value pairs in a JSON Object.
*
* @param {Snapshot} [snapshot]
* @return {{ [Key in Extract<keyof Attrs,string>]?: Attrs[Key]}} A JSON Object that describes the attributes.
*
* @public
*/
getAttributes (snapshot) {
return /** @type {any} */ (snapshot ? typeMapGetAllSnapshot(this, snapshot) : typeMapGetAll(this))
}
/**
* Transform the properties of this type to binary and write it to an
* BinaryEncoder.
*
* This is called when this Item is sent to a remote peer.
*
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
*/
_write (encoder) {
encoder.writeTypeRef(YXmlElementRefID)
encoder.writeKey(this.nodeName)
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {import('../utils/types.js').YType}
*
* @function
*/
export const readYXmlElement = decoder => new YXmlElement(decoder.readKey())

View File

@@ -1,266 +0,0 @@
/**
* @module YXml
*/
import {
AbstractType,
typeListMap,
typeListForEach,
typeListInsertGenerics,
typeListInsertGenericsAfter,
typeListDelete,
typeListToArray,
YXmlFragmentRefID,
transact,
typeListGet,
typeListSlice,
warnPrematureAccess,
YXmlElement, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, YXmlText, YXmlHook // eslint-disable-line
} from '../internals.js'
import * as delta from 'lib0/delta' // eslint-disable-line
import * as error from 'lib0/error'
/**
* Define the elements to which a set of CSS queries apply.
* {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors}
*
* @example
* query = '.classSelector'
* query = 'nodeSelector'
* query = '#idSelector'
*
* @typedef {string} CSS_Selector
*/
/**
* Dom filter function.
*
* @callback domFilter
* @param {string} nodeName The nodeName of the element
* @param {Map} attributes The map of attributes.
* @return {boolean} Whether to include the Dom node in the YXmlElement.
*/
/**
* Represents a list of {@link YXmlElement}.and {@link YXmlText} types.
* A YxmlFragment is similar to a {@link YXmlElement}, but it does not have a
* nodeName and it does not have attributes. Though it can be bound to a DOM
* element - in this case the attributes and the nodeName are not shared.
*
* @public
* @template {any} [Children=any]
* @template {{[K in string]:any}} [Attrs={}]
* @extends AbstractType<delta.Delta<any,Attrs,Children,any>>
*/
export class YXmlFragment extends AbstractType {
constructor () {
super()
/**
* @todo remove _prelimContent
* @type {Array<any>|null}
*/
this._prelimContent = []
}
/**
* @type {YXmlElement|YXmlText|null}
*/
get firstChild () {
const first = this._first
return first ? first.content.getContent()[0] : null
}
/**
* Integrate this type into the Yjs instance.
*
* * Save this struct in the os
* * This type is sent to other client
* * Observer functions are fired
*
* @param {Doc} y The Yjs instance
* @param {Item?} item
*/
_integrate (y, item) {
super._integrate(y, item)
this.insert(0, /** @type {Array<any>} */ (this._prelimContent))
this._prelimContent = null
}
/**
* Makes a copy of this data type that can be included somewhere else.
*
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
*
* @return {this}
*/
clone () {
const el = this._copy()
el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
return el
}
get length () {
this.doc ?? warnPrematureAccess()
return this._prelimContent === null ? this._length : this._prelimContent.length
}
/**
* Get the string representation of all the children of this YXmlFragment.
*
* @return {string} The string representation of all children.
*/
toString () {
return typeListMap(this, xml => xml.toString()).join('')
}
/**
* @return {string}
*/
toJSON () {
return this.toString()
}
/**
* Inserts new content at an index.
*
* @example
* // Insert character 'a' at position 0
* xml.insert(0, [new Y.XmlText('text')])
*
* @param {number} index The index to insert content at
* @param {Array<YXmlElement|YXmlText|YXmlHook>} content The array of content
*/
insert (index, content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListInsertGenerics(transaction, this, index, content)
})
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, 0, ...content)
}
}
/**
* Inserts new content at an index.
*
* @example
* // Insert character 'a' at position 0
* xml.insert(0, [new Y.XmlText('text')])
*
* @param {null|Item|YXmlElement|YXmlText} ref The index to insert content at
* @param {Array<YXmlElement|YXmlText>} content The array of content
*/
insertAfter (ref, content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
const refItem = (ref && ref instanceof AbstractType) ? ref._item : ref
typeListInsertGenericsAfter(transaction, this, refItem, content)
})
} else {
const pc = /** @type {Array<any>} */ (this._prelimContent)
const index = ref === null ? 0 : pc.findIndex(el => el === ref) + 1
if (index === 0 && ref !== null) {
throw error.create('Reference item not found')
}
pc.splice(index, 0, ...content)
}
}
/**
* Deletes elements starting from an index.
*
* @param {number} index Index at which to start deleting elements
* @param {number} [length=1] The number of elements to remove. Defaults to 1.
*/
delete (index, length = 1) {
if (this.doc !== null) {
transact(this.doc, transaction => {
typeListDelete(transaction, this, index, length)
})
} else {
// @ts-ignore _prelimContent is defined because this is not yet integrated
this._prelimContent.splice(index, length)
}
}
/**
* Transforms this YArray to a JavaScript Array.
*
* @return {Array<YXmlElement|YXmlText|YXmlHook>}
*/
toArray () {
return typeListToArray(this)
}
/**
* Appends content to this YArray.
*
* @param {Array<YXmlElement|YXmlText>} content Array of content to append.
*/
push (content) {
this.insert(this.length, content)
}
/**
* Prepends content to this YArray.
*
* @param {Array<YXmlElement|YXmlText>} content Array of content to prepend.
*/
unshift (content) {
this.insert(0, content)
}
/**
* Returns the i-th element from a YArray.
*
* @param {number} index The index of the element to return from the YArray
* @return {YXmlElement|YXmlText}
*/
get (index) {
return typeListGet(this, index)
}
/**
* Returns a portion of this YXmlFragment into a JavaScript Array selected
* from start to end (end not included).
*
* @param {number} [start]
* @param {number} [end]
* @return {Array<YXmlElement|YXmlText>}
*/
slice (start = 0, end = this.length) {
return typeListSlice(this, start, end)
}
/**
* Executes a provided function on once on every child element.
*
* @param {function(YXmlElement|YXmlText,number, typeof self):void} f A function to execute on every element of this YArray.
*/
forEach (f) {
typeListForEach(this, f)
}
/**
* Transform the properties of this type to binary and write it to an
* BinaryEncoder.
*
* This is called when this Item is sent to a remote peer.
*
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
*/
_write (encoder) {
encoder.writeTypeRef(YXmlFragmentRefID)
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
* @return {import('../utils/types.js').YType}
*
* @private
* @function
*/
export const readYXmlFragment = _decoder => new YXmlFragment()

View File

@@ -1,68 +0,0 @@
import {
YMap,
YXmlHookRefID,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2 // eslint-disable-line
} from '../internals.js'
/**
* You can manage binding to a custom type with YXmlHook.
*
* @extends {YMap<any>}
*/
export class YXmlHook extends YMap {
/**
* @param {string} hookName nodeName of the Dom Node.
*/
constructor (hookName) {
super()
/**
* @type {string}
*/
this.hookName = hookName
}
/**
* @return {this}
*/
_copy () {
return /** @type {this} */ (new YXmlHook(this.hookName))
}
/**
* Makes a copy of this data type that can be included somewhere else.
*
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
*
* @return {this}
*/
clone () {
const el = this._copy()
this.forEach((value, key) => {
el.set(key, value)
})
return el
}
/**
* Transform the properties of this type to binary and write it to an
* BinaryEncoder.
*
* This is called when this Item is sent to a remote peer.
*
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to.
*/
_write (encoder) {
encoder.writeTypeRef(YXmlHookRefID)
encoder.writeKey(this.hookName)
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {import('../utils/types.js').YType}
*
* @private
* @function
*/
export const readYXmlHook = decoder =>
new YXmlHook(decoder.readKey())

View File

@@ -1,66 +0,0 @@
import {
YText,
YXmlTextRefID,
ContentType, YXmlElement, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, // eslint-disable-line
} from '../internals.js'
/**
* @todo can we deprecate this?
*
* Represents text in a Dom Element. In the future this type will also handle
* simple formatting information like bold and italic.
* @extends YText
*/
export class YXmlText extends YText {
/**
* @type {YXmlElement|YXmlText|null}
*/
get nextSibling () {
const n = this._item ? this._item.next : null
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
}
/**
* @type {YXmlElement|YXmlText|null}
*/
get prevSibling () {
const n = this._item ? this._item.prev : null
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
}
/**
* Makes a copy of this data type that can be included somewhere else.
*
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
*
* @return {this}
*/
clone () {
const text = /** @type {this} */ (this._copy())
text.applyDelta(this.getContent())
return text
}
/**
* @return {string}
*/
toJSON () {
return this.toString()
}
/**
* @param {UpdateEncoderV1 | UpdateEncoderV2} encoder
*/
_write (encoder) {
encoder.writeTypeRef(YXmlTextRefID)
}
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
* @return {import('../utils/types.js').YType}
*
* @private
* @function
*/
export const readYXmlText = _decoder => new YXmlText()

View File

@@ -4,13 +4,13 @@
import {
StructStore,
YType,
transact,
applyUpdate,
ContentDoc, Item, Transaction, // eslint-disable-line
encodeStateAsUpdate
} from '../internals.js'
import { YType } from '../ytype.js'
import { ObservableV2 } from 'lib0/observable'
import * as random from 'lib0/random'
import * as map from 'lib0/map'
@@ -190,22 +190,18 @@ export class Doc extends ObservableV2 {
/**
* Define a shared data type.
*
* Multiple calls of `ydoc.get(name, TypeConstructor)` yield the same result
* Multiple calls of `ydoc.get(name)` yield the same result
* and do not overwrite each other. I.e.
* `ydoc.get(name, Y.Array) === ydoc.get(name, Y.Array)`
* `ydoc.get(name) === ydoc.get(name)`
*
* After this method is called, the type is also available on `ydoc.share.get(name)`.
*
* *Best Practices:*
* Define all types right after the Y.Doc instance is created and store them in a separate object.
* Also use the typed methods `getText(name)`, `getArray(name)`, ..
*
* @param {string} key
* @param {string?} name Type-name
*
* @return {YType}
*/
get (key, name = null) {
get (key = '', name = null) {
return map.setIfUndefined(this.share, key, () => {
const t = new YType(name)
t._integrate(this, null)

View File

@@ -14,7 +14,7 @@ import {
IdSet, UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractStruct, YEvent, Doc // eslint-disable-line
} from '../internals.js'
import {YType} from '../ytype.js'
import { YType } from '../ytype.js' // eslint-disable-line
import * as error from 'lib0/error'
import * as map from 'lib0/map'
import * as math from 'lib0/math'
@@ -343,7 +343,6 @@ const updateCurrentAttributes = (currentAttributes, { key, value }) => {
}
}
/**
* Call this function after string content has been deleted in order to
* clean up formatting Items.
@@ -411,7 +410,6 @@ export const cleanupFormattingGap = (transaction, start, curr, startAttributes,
return cleanups
}
/**
* This function is experimental and subject to change / be removed.
*
@@ -451,7 +449,6 @@ export const cleanupYTextFormatting = type => {
return res
}
/**
* This will be called by the transaction once the event handlers are called to potentially cleanup
* formatting attributes.

View File

@@ -39,7 +39,7 @@ export class YEvent {
*/
this.transaction = transaction
/**
* @type {import('../ytype.js').DeltaConfTypesToDelta<DConf>|null}
* @type {delta.Delta<import('../ytype.js').DeltaConfDeltaToYType<DConf>>|null}
*/
this._delta = null
/**
@@ -116,7 +116,7 @@ export class YEvent {
* @param {AbstractAttributionManager} am
* @param {object} [opts]
* @param {Deep} [opts.deep]
* @return {Deep extends true ? delta.Delta<import('../internals.js').DeltaConfTypesToDelta<DConf>> : delta.Delta<DConf>} The Delta representation of this type.
* @return {Deep extends true ? delta.Delta<DConf> : delta.Delta<import('../internals.js').DeltaConfDeltaToYType<DConf>>} The Delta representation of this type.
*
* @public
*/
@@ -155,7 +155,7 @@ export class YEvent {
* Compute the changes in the delta format.
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
*
* @type {delta.Delta<DConf>} The Delta representation of this type.
* @type {delta.Delta<import('../internals.js').DeltaConfDeltaToYType<DConf>>} The Delta representation of this type.
* @public
*/
get delta () {
@@ -166,7 +166,7 @@ export class YEvent {
* Compute the changes in the delta format.
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
*
* @type {import('../internals.js').DeltaConfTypesToDeltaDelta<DConf>} The Delta representation of this type.
* @type {delta.Delta<DConf>} The Delta representation of this type.
* @public
*/
get deltaDeep () {
@@ -200,7 +200,7 @@ export const getPathTo = (parent, child, am = noAttributionsManager) => {
// parent is map-ish
path.unshift(child._item.parentSub)
} else {
const parent = /** @type {import('../utils/types.js').YType} */ (child._item.parent)
const parent = /** @type {import('../ytype.js').YType} */ (child._item.parent)
// parent is array-ish
const apos = /** @type {AbsolutePosition} */ (createAbsolutePositionFromRelativePosition(createRelativePosition(parent, child._item.id), doc, false, am))
path.unshift(apos.index)

View File

@@ -1,3 +1,3 @@
/**
* @typedef {Object<string,any>|Array<any>|number|null|string|Uint8Array|BigInt|import('./').YType} YValue
* @typedef {Object<string,any>|Array<any>|number|null|string|Uint8Array|BigInt|import('../ytype.js').YType} YValue
*/

View File

@@ -584,13 +584,13 @@ export const convertUpdateFormat = (update, blockTransformer, YDecoder, YEncoder
* @typedef {Object} ObfuscatorOptions
* @property {boolean} [ObfuscatorOptions.formatting=true]
* @property {boolean} [ObfuscatorOptions.subdocs=true]
* @property {boolean} [ObfuscatorOptions.yxml=true] Whether to obfuscate nodeName / hookName
* @property {boolean} [ObfuscatorOptions.name=true] Whether to obfuscate nodeName / hookName
*/
/**
* @param {ObfuscatorOptions} obfuscator
*/
const createObfuscator = ({ formatting = true, subdocs = true, yxml = true } = {}) => {
const createObfuscator = ({ formatting = true, subdocs = true, name = true } = {}) => {
let i = 0
const mapKeyCache = map.create()
const nodeNameCache = map.create()
@@ -613,10 +613,10 @@ const createObfuscator = ({ formatting = true, subdocs = true, yxml = true } = {
case ContentDeleted:
break
case ContentType: {
if (yxml) {
if (name) {
const type = /** @type {ContentType} */ (content).type
if (type.name != null) {
type.name = map.setIfUndefined(nodeNameCache, type.nodeName, () => 'typename-' + i)
type.name = map.setIfUndefined(nodeNameCache, type.name, () => 'typename-' + i)
}
}
break

View File

@@ -184,63 +184,6 @@ export class ItemTextListPosition {
}
}
/**
* @param {Transaction} transaction
* @param {ItemTextListPosition} pos
* @param {number} count steps to move forward
* @return {ItemTextListPosition}
*
* @private
* @function
*/
const findNextPosition = (transaction, pos, count) => {
while (pos.right !== null && count > 0) {
switch (pos.right.content.constructor) {
case ContentFormat:
if (!pos.right.deleted) {
updateCurrentAttributes(pos.currentAttributes, /** @type {ContentFormat} */ (pos.right.content))
}
break
default:
if (!pos.right.deleted) {
if (count < pos.right.length) {
// split right
getItemCleanStart(transaction, createID(pos.right.id.client, pos.right.id.clock + count))
}
pos.index += pos.right.length
count -= pos.right.length
}
break
}
pos.left = pos.right
pos.right = pos.right.right
// pos.forward() - we don't forward because that would halve the performance because we already do the checks above
}
return pos
}
/**
* @param {Transaction} transaction
* @param {YType} parent
* @param {number} index
* @param {boolean} useSearchMarker
* @return {ItemTextListPosition}
*
* @private
* @function
*/
const findPosition = (transaction, parent, index, useSearchMarker) => {
const currentAttributes = new Map()
const marker = useSearchMarker ? findMarker(parent, index) : null
if (marker) {
const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes, noAttributionsManager)
return findNextPosition(transaction, pos, index - marker.index)
} else {
const pos = new ItemTextListPosition(null, parent._start, 0, currentAttributes, noAttributionsManager)
return findNextPosition(transaction, pos, index)
}
}
/**
* Negate applied formats
*
@@ -696,12 +639,22 @@ export class YType {
this._hasFormatting = false
}
/**
* @template {delta.DeltaConf} DC
* @param {delta.Delta<DC>} d
* @return {YType<DC>}
*/
static from (d) {
const yt = new YType(d.name)
yt.applyDelta(d)
return yt
}
get length () {
this.doc ?? warnPrematureAccess()
return this._length
}
/**
* Returns a fresh delta that can be used to change this YType.
* @type {delta.DeltaBuilder<DeltaToYType<DConf>>}
@@ -1075,13 +1028,7 @@ export class YType {
for (let i = 0; i < op.insert.length; i++) {
let ins = op.insert[i]
if (delta.$deltaAny.check(ins)) {
if (ins.name != null) {
const t = new YType(ins.name)
t.applyDelta(ins)
ins = t
} else {
error.unexpectedCase()
}
ins = YType.from(ins)
}
insertText(transaction, /** @type {any} */ (this), currPos, ins, op.format || {})
}
@@ -1103,7 +1050,7 @@ export class YType {
for (const op of d.attrs) {
if (delta.$setAttrOp.check(op)) {
typeMapSet(transaction, /** @type {any} */ (this), /** @type {any} */ (op.key), op.value)
} else if (delta.$deleteOp.check(op)) {
} else if (delta.$deleteAttrOp.check(op)) {
typeMapDelete(transaction, /** @type {any} */ (this), /** @type {any} */ (op.key))
} else {
const sub = typeMapGet(/** @type {any} */ (this), /** @type {any} */ (op.key))
@@ -1115,6 +1062,7 @@ export class YType {
}
})
}
return this
}
/**
@@ -1134,14 +1082,13 @@ export class YType {
* Removes all elements from this YMap.
*/
clearAttrs () {
let d = delta.create()
this.forEachAttr((_,key) => {
const d = delta.create()
this.forEachAttr((_, key) => {
d.deleteAttr(/** @type {any} */ (key))
})
this.applyDelta(d)
}
/**
* Removes an attribute from this YXmlElement.
*
@@ -1163,8 +1110,9 @@ export class YType {
*
* @public
*/
setAttribute (attributeName, attributeValue) {
setAttr (attributeName, attributeValue) {
this.applyDelta(delta.create().setAttr(attributeName, attributeValue).done())
return attributeValue
}
/**
@@ -1175,7 +1123,7 @@ export class YType {
* @return {delta.DeltaConfGetAttrs<DConf>[KEY]|undefined} The queried attribute value.
* @public
*/
getAttribute (attributeName) {
getAttr (attributeName) {
return /** @type {any} */ (typeMapGet(this, attributeName))
}
@@ -1187,7 +1135,7 @@ export class YType {
*
* @public
*/
hasAttribute (attributeName) {
hasAttr (attributeName) {
return /** @type {any} */ (typeMapHas(this, attributeName))
}
@@ -1199,7 +1147,7 @@ export class YType {
*
* @public
*/
getAttributes (snapshot) {
getAttrs (snapshot) {
return /** @type {any} */ (snapshot ? typeMapGetAllSnapshot(this, snapshot) : typeMapGetAll(this))
}
@@ -1218,9 +1166,54 @@ export class YType {
*
* @param {number} index The index to insert content at.
* @param {Array<delta.DeltaConfGetChildren<DConf>>|delta.DeltaConfGetText<DConf>} content Array of content to append.
* @param {delta.FormattingAttributes} [format]
*/
insert (index, content) {
this.applyDelta(delta.create().retain(index).insert(/** @type {any} */ (content)))
insert (index, content, format) {
this.applyDelta(delta.create().retain(index).insert(/** @type {any} */ (content), format))
}
/**
* Inserts new content at an index.
*
* Important: This function expects an array of content. Not just a content
* object. The reason for this "weirdness" is that inserting several elements
* is very efficient when it is done as a single operation.
*
* @example
* // Insert character 'a' at position 0
* yarray.insert(0, ['a'])
* // Insert numbers 1, 2 at position 1
* yarray.insert(1, [1, 2])
*
* @param {number} index The index to insert content at.
* @param {number} length The index to insert content at.
* @param {delta.FormattingAttributes} formats
*
*/
format (index, length, formats) {
this.applyDelta(delta.create().retain(index).retain(length, formats))
}
/**
* Inserts new content after another element.
*
* @example
* // Insert character 'a' at position 0
* xml.insert(0, [new Y.XmlText('text')])
*
* @param {null|Item|YType} ref The index to insert content at
* @param {Array<delta.DeltaConfGetChildren<DConf>>} content The array of content
*/
insertAfter (ref, content) {
if (this.doc !== null) {
transact(this.doc, transaction => {
const refItem = ref && ref instanceof YType ? ref._item : ref
typeListInsertGenericsAfter(transaction, this, refItem, content)
})
} else {
// only possible once this item has been integrated
error.unexpectedCase()
}
}
/**
@@ -1275,21 +1268,38 @@ export class YType {
return typeListSlice(this, start, end)
}
/**
* @todo refactor this, this should use getContent only!
*
* Transforms this YArray to a JavaScript Array.
*
* @return {Array<delta.DeltaConfGetChildren<DConf>>}
* @return {Array<delta.DeltaConfGetChildren<DConf> | delta.DeltaConfGetText<DConf>>}
*/
toArray () {
return typeListToArray(this)
const dcontent = this.getContent()
/**
* @type {Array<any>}
*/
const children = []
for (const child of dcontent.children) {
if (delta.$insertOp.check(child) || delta.$textOp.check(child)) {
children.push(child.insert)
}
}
return children
}
/**
* Transforms this Shared Type to a JSON object.
*/
toJSON () {
return this.getContent().toJSON()
const attrs = this.getAttrs()
const children = this.toArray()
return {
name: this.name,
children,
attrs
}
}
/**
@@ -1297,22 +1307,21 @@ export class YType {
* child-element.
*
* @template M
* @param {(child:delta.DeltaConfGetChildren<DConf>,index:number,ytype:this)=>M} f Function that produces an element of the new Array
* @param {(child:delta.DeltaConfGetChildren<DConf>|delta.DeltaConfGetText<DConf>,index:number)=>M} f Function that produces an element of the new Array
* @return {Array<M>} A new array with each element being the result of the
* callback function
*/
map (f) {
return typeListMap(this, /** @type {any} */ (f))
return this.toArray().map(f)
}
/**
* Executes a provided function once on every element of this YArray.
*
* @template M
* @param {(child:delta.DeltaConfGetChildren<DConf>,index:number,ytype:this)=>M} f Function that produces an element of the new Array
* @param {(child:delta.DeltaConfGetChildren<DConf>|delta.DeltaConfGetText<DConf>,index:number)=>any} f Function that produces an element of the new Array
*/
forEach (f) {
typeListForEach(this, f)
return this.toArray().forEach(f)
}
/**
@@ -1328,19 +1337,10 @@ export class YType {
})
}
/**
* @return {IterableIterator<delta.DeltaConfGetChildren<DConf>>}
*/
[Symbol.iterator] () {
return typeListCreateIterator(this)
}
/**
* Returns the keys for each element in the YMap Type.
*
* @return {IterableIterator<keyof delta.DeltaConfGetAttrs<DConf>>}
* @return {IterableIterator<import('lib0/ts').KeyOf<delta.DeltaConfGetAttrs<DConf>>>}
*/
attrKeys () {
return iterator.iteratorMap(createMapIterator(this), /** @param {any} v */ v => v[0])
@@ -1358,7 +1358,7 @@ export class YType {
/**
* Returns an Iterator of [key, value] pairs
*
* @return {IterableIterator<{ [K in keyof delta.DeltaConfGetAttrs<DConf>]: [K,delta.DeltaConfGetAttrs<DConf>] }[any]>}
* @return {IterableIterator<{ [K in keyof delta.DeltaConfGetAttrs<DConf>]: [K,delta.DeltaConfGetAttrs<DConf>[K]] }[any]>}
*/
attrEntries () {
return iterator.iteratorMap(createMapIterator(this), /** @param {any} v */ v => /** @type {any} */ ([v[0], v[1].content.getContent()[v[1].length - 1]]))
@@ -1369,7 +1369,7 @@ export class YType {
*
* @return {number}
*/
attrSize () {
get attrSize () {
return [...createMapIterator(this)].length
}
@@ -1379,7 +1379,7 @@ export class YType {
[traits.EqualityTraitSymbol] (other) {
return this.getContent().equals(other.getContent())
}
/**
* @todo this doesn't need to live in a method.
*
@@ -1475,145 +1475,6 @@ export const typeListSlice = (type, start, end) => {
return cs
}
/**
* @param {YType} type
* @return {Array<any>}
*
* @private
* @function
*/
export const typeListToArray = type => {
type.doc ?? warnPrematureAccess()
const cs = []
let n = type._start
while (n !== null) {
if (n.countable && !n.deleted) {
const c = n.content.getContent()
for (let i = 0; i < c.length; i++) {
cs.push(c[i])
}
}
n = n.right
}
return cs
}
/**
* @param {YType<any>} type
* @param {Snapshot} snapshot
* @return {Array<any>}
*
* @private
* @function
*/
export const typeListToArraySnapshot = (type, snapshot) => {
const cs = []
let n = type._start
while (n !== null) {
if (n.countable && isVisible(n, snapshot)) {
const c = n.content.getContent()
for (let i = 0; i < c.length; i++) {
cs.push(c[i])
}
}
n = n.right
}
return cs
}
/**
* Executes a provided function on once on every element of this YArray.
*
* @param {YType<any>} type
* @param {function(any,number,any):void} f A function to execute on every element of this YArray.
*
* @private
* @function
*/
export const typeListForEach = (type, f) => {
let index = 0
let n = type._start
type.doc ?? warnPrematureAccess()
while (n !== null) {
if (n.countable && !n.deleted) {
const c = n.content.getContent()
for (let i = 0; i < c.length; i++) {
f(c[i], index++, type)
}
}
n = n.right
}
}
/**
* @template C,R
* @param {YType<any>} type
* @param {function(C,number,YType<any>):R} f
* @return {Array<R>}
*
* @private
* @function
*/
export const typeListMap = (type, f) => {
/**
* @type {Array<any>}
*/
const result = []
typeListForEach(type, (c, i) => {
result.push(f(c, i, type))
})
return result
}
/**
* @param {YType} type
* @return {IterableIterator<any>}
*
* @private
* @function
*/
export const typeListCreateIterator = type => {
let n = type._start
/**
* @type {Array<any>|null}
*/
let currentContent = null
let currentContentIndex = 0
return {
[Symbol.iterator] () {
return this
},
next: () => {
// find some content
if (currentContent === null) {
while (n !== null && n.deleted) {
n = n.right
}
// check if we reached the end, no need to check currentContent, because it does not exist
if (n === null) {
return {
done: true,
value: undefined
}
}
// we found n, so we can set currentContent
currentContent = n.content.getContent()
currentContentIndex = 0
n = n.right // we used the content of n, now iterate to next
}
const value = currentContent[currentContentIndex++]
// check if we need to empty currentContent
if (currentContent.length <= currentContentIndex) {
currentContent = null
}
return {
done: false,
value
}
}
}
}
/**
* @todo remove / inline this
*

View File

@@ -14,7 +14,7 @@ import * as delta from 'lib0/delta'
*/
export const testRelativePositions = _tc => {
const ydoc = new Y.Doc()
const ytext = ydoc.getText()
const ytext = ydoc.get()
ytext.insert(0, 'hello world')
const v1 = Y.cloneDoc(ydoc)
ytext.delete(1, 6)
@@ -32,7 +32,7 @@ export const testRelativePositions = _tc => {
*/
export const testAttributedEvents = _tc => {
const ydoc = new Y.Doc()
const ytext = ydoc.getText()
const ytext = ydoc.get()
ytext.insert(0, 'hello world')
const v1 = Y.cloneDoc(ydoc)
ydoc.transact(() => {
@@ -56,7 +56,7 @@ export const testAttributedEvents = _tc => {
*/
export const testInsertionsMindingAttributedContent = _tc => {
const ydoc = new Y.Doc()
const ytext = ydoc.getText()
const ytext = ydoc.get()
ytext.insert(0, 'hello world')
const v1 = Y.cloneDoc(ydoc)
ydoc.transact(() => {
@@ -74,7 +74,7 @@ export const testInsertionsMindingAttributedContent = _tc => {
*/
export const testInsertionsIntoAttributedContent = _tc => {
const ydoc = new Y.Doc()
const ytext = ydoc.getText()
const ytext = ydoc.get()
ytext.insert(0, 'hello ')
const v1 = Y.cloneDoc(ydoc)
ydoc.transact(() => {
@@ -89,15 +89,15 @@ export const testInsertionsIntoAttributedContent = _tc => {
export const testYdocDiff = () => {
const ydocStart = new Y.Doc()
ydocStart.getText('text').insert(0, 'hello')
ydocStart.getArray('array').insert(0, [1, 2, 3])
ydocStart.getMap('map').set('k', 42)
ydocStart.getMap('map').set('nested', new Y.Array())
ydocStart.get('text').insert(0, 'hello')
ydocStart.get('array').insert(0, [1, 2, 3])
ydocStart.get('map').setAttr('k', 42)
ydocStart.get('map').setAttr('nested', new Y.Type())
const ydocUpdated = Y.cloneDoc(ydocStart)
ydocUpdated.getText('text').insert(5, ' world')
ydocUpdated.getArray('array').insert(1, ['x'])
ydocUpdated.getMap('map').set('newk', 42)
ydocUpdated.getMap('map').get('nested').insert(0, [1])
ydocUpdated.get('text').insert(5, ' world')
ydocUpdated.get('array').insert(1, ['x'])
ydocUpdated.get('map').setAttr('newk', 42)
ydocUpdated.get('map').getAttr('nested').insert(0, [1])
// @todo add custom attribution
const d = Y.diffDocsToDelta(ydocStart, ydocUpdated)
console.log('calculated diff', d.toJSON())
@@ -111,19 +111,18 @@ export const testYdocDiff = () => {
export const testChildListContent = () => {
const ydocStart = new Y.Doc()
const ydocUpdated = Y.cloneDoc(ydocStart)
const yf = new Y.XmlElement('test')
const yf = new Y.Type('test')
let calledEvent = 0
yf.applyDelta(delta.create().insert('test content').setAttr('k', 'v'))
const yarray = ydocUpdated.getArray('array')
yarray.observeDeep((events, tr) => {
const yarray = ydocUpdated.get('array')
yarray.observeDeep(event => {
calledEvent++
const event = events.find(event => event.target === yarray) || new Y.YEvent(yarray, tr, new Set(null))
const d = event.deltaDeep
const expectedD = delta.create().insert([delta.create('test').insert('test content').setAttr('k', 'v')])
t.compare(d, expectedD)
})
ydocUpdated.getArray('array').insert(0, [yf])
ydocUpdated.get('array').insert(0, [yf])
t.assert(calledEvent === 1)
const d = Y.diffDocsToDelta(ydocStart, ydocUpdated)
console.log('calculated diff', d.toJSON())

View File

@@ -6,7 +6,7 @@ import * as t from 'lib0/testing'
*/
export const testAfterTransactionRecursion = _tc => {
const ydoc = new Y.Doc()
const yxml = ydoc.getXmlFragment('')
const yxml = ydoc.get('')
ydoc.on('afterTransaction', tr => {
if (tr.origin === 'test') {
yxml.toJSON()
@@ -14,7 +14,7 @@ export const testAfterTransactionRecursion = _tc => {
})
ydoc.transact(_tr => {
for (let i = 0; i < 15000; i++) {
yxml.push([new Y.XmlText('a')])
yxml.push([new Y.Type('a')])
}
}, 'test')
}
@@ -24,12 +24,12 @@ export const testAfterTransactionRecursion = _tc => {
*/
export const testFindTypeInOtherDoc = _tc => {
const ydoc = new Y.Doc()
const ymap = ydoc.getMap()
const ytext = ymap.set('ytext', new Y.Text())
const ymap = ydoc.get()
const ytext = ymap.setAttr('ytext', new Y.Type())
const ydocClone = new Y.Doc()
Y.applyUpdate(ydocClone, Y.encodeStateAsUpdate(ydoc))
/**
* @template {import('../src/utils/types.js').YType} Type
* @template {Y.Type} Type
* @param {Type} ytype
* @param {Y.Doc} otherYdoc
* @return {Type}
@@ -47,7 +47,7 @@ export const testFindTypeInOtherDoc = _tc => {
if (rootKey == null) {
throw new Error('type does not exist in other ydoc')
}
return /** @type {Type} */ (otherYdoc.get(rootKey, /** @type {import('../src/utils/types.js').YTypeConstructors} */ (ytype.constructor)))
return /** @type {Type} */ (otherYdoc.get(rootKey, /** @type {import('../src/utils/ts.js').YTypeConstructors} */ (ytype.constructor)))
} else {
/**
* If it is a sub type, we use the item id to find the history type.

View File

@@ -24,9 +24,9 @@ export const testBasicXmlAttributes = _tc => {
yxml.setAttribute('a', '1')
const snapshot2 = Y.snapshot(ydoc)
yxml.setAttribute('a', '2')
t.compare(yxml.getAttributes(), { a: '2' })
t.compare(yxml.getAttributes(snapshot2), { a: '1' })
t.compare(yxml.getAttributes(snapshot1), {})
t.compare(yxml.getAttrs(), { a: '2' })
t.compare(yxml.getAttrs(snapshot2), { a: '1' })
t.compare(yxml.getAttrs(snapshot1), {})
}
/**

View File

@@ -270,7 +270,7 @@ export class TestConnector {
* @param {t.TestCase} tc
* @param {{users?:number}} conf
* @param {InitTestObjectCallback<T>} [initTestObject]
* @return {{testObjects:Array<any>,testConnector:TestConnector,users:Array<TestYInstance>,array0:Y.Array<any>,array1:Y.Array<any>,array2:Y.Array<any>,map0:Y.Map<any>,map1:Y.Map<any>,map2:Y.Map<any>,map3:Y.Map<any>,text0:Y.Text,text1:Y.Text,text2:Y.Text,xml0:Y.XmlElement,xml1:Y.XmlElement,xml2:Y.XmlElement}}
* @return {{testObjects:Array<any>,testConnector:TestConnector,users:Array<TestYInstance>,array0:Y.Type<any>,array1:Y.Type<any>,array2:Y.Type<any>,map0:Y.Type<any>,map1:Y.Type<any>,map2:Y.Type<any>,map3:Y.Type<any>,text0:Y.Type,text1:Y.Type,text2:Y.Type,xml0:Y.Type,xml1:Y.Type,xml2:Y.Type}}
*/
export const init = (tc, { users = 5 } = {}, initTestObject) => {
/**
@@ -293,10 +293,10 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => {
const y = testConnector.createY(i)
y.clientID = i
result.users.push(y)
result['array' + i] = y.getArray('array')
result['map' + i] = y.getMap('map')
result['xml' + i] = y.get('xml', Y.XmlElement)
result['text' + i] = y.getText('text')
result['array' + i] = y.get('array')
result['map' + i] = y.get('map')
result['xml' + i] = y.get('xml')
result['text' + i] = y.get('text')
}
testConnector.syncAll()
result.testObjects = result.users.map(initTestObject || (() => null))
@@ -458,37 +458,37 @@ export const compare = users => {
return ydoc
})
users.push(.../** @type {any} */(mergedDocs))
const userArrayValues = users.map(u => u.getArray('array').toJSON())
const userMapValues = users.map(u => u.getMap('map').toJSON())
const userArrayValues = users.map(u => u.get('array').toJSON().children || [])
const userMapValues = users.map(u => u.get('map').toJSON())
// @todo fix type error here
// @ts-ignore
const userXmlValues = users.map(u => /** @type {Y.XmlElement} */ (u.get('xml', Y.XmlElement)).toString())
const userTextValues = users.map(u => u.getText('text').getContentDeep())
const userTextValues = users.map(u => u.get('text').getContentDeep())
for (const u of users) {
t.assert(u.store.pendingDs === null)
t.assert(u.store.pendingStructs === null)
}
// Test Array iterator
t.compare(users[0].getArray('array').toArray(), Array.from(users[0].getArray('array')))
t.compare(users[0].get('array').toArray(), Array.from(users[0].get('array')))
// Test Map iterator
const ymapkeys = Array.from(users[0].getMap('map').keys())
const ymapkeys = Array.from(users[0].get('map').attrKeys())
t.assert(ymapkeys.length === Object.keys(userMapValues[0]).length)
ymapkeys.forEach(key => t.assert(object.hasProperty(userMapValues[0], key)))
/**
* @type {Object<string,any>}
*/
const mapRes = {}
for (const [k, v] of users[0].getMap('map')) {
for (const [k, v] of users[0].get('map')) {
mapRes[k] = v instanceof Y.AbstractType ? v.toJSON() : v
}
t.compare(userMapValues[0], mapRes)
// Compare all users
for (let i = 0; i < users.length - 1; i++) {
t.compare(userArrayValues[i].length, users[i].getArray('array').length)
t.compare(userArrayValues[i].length, users[i].get('array').length)
t.compare(userArrayValues[i], userArrayValues[i + 1])
t.compare(userMapValues[i], userMapValues[i + 1])
t.compare(userXmlValues[i], userXmlValues[i + 1])
t.compare(list.toArray(userTextValues[i].children).map(a => (delta.$textOp.check(a) || delta.$insertOp.check(a)) ? a.insert.length : 0).reduce((a, b) => a + b, 0), users[i].getText('text').length)
t.compare(list.toArray(userTextValues[i].children).map(a => (delta.$textOp.check(a) || delta.$insertOp.check(a)) ? a.insert.length : 0).reduce((a, b) => a + b, 0), users[i].get('text').length)
t.compare(userTextValues[i], userTextValues[i + 1], '', (_constructor, a, b) => {
if (a instanceof Y.AbstractType) {
t.compare(a.toJSON(), b.toJSON())

View File

@@ -111,7 +111,7 @@ export const testMergeUpdates = tc => {
compare(users)
encoders.forEach(enc => {
const merged = fromUpdates(users, enc)
t.compareArrays(array0.toArray(), merged.getArray('array').toArray())
t.compareArrays(array0.toArray(), merged.get('array').toArray())
})
}
@@ -184,7 +184,7 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => {
// enc.logUpdate(updates)
const merged = new Y.Doc({ gc: false })
enc.applyUpdate(merged, mergedUpdates)
t.compareArrays(merged.getArray().toArray(), ydoc.getArray().toArray())
t.compareArrays(merged.get().toArray(), ydoc.get().toArray())
t.compare(enc.encodeStateVector(merged), enc.encodeStateVectorFromUpdate(mergedUpdates))
if (enc.updateEventName !== 'update') { // @todo should this also work on legacy updates?
for (let j = 1; j < updates.length; j++) {
@@ -235,7 +235,7 @@ export const testMergeUpdates1 = _tc => {
const ydoc = new Y.Doc({ gc: false })
const updates = /** @type {Array<Uint8Array<ArrayBuffer>>} */ ([])
ydoc.on(enc.updateEventName, update => { updates.push(update) })
const array = ydoc.getArray()
const array = ydoc.get()
array.insert(0, [1])
array.insert(0, [2])
array.insert(0, [3])
@@ -253,7 +253,7 @@ export const testMergeUpdates2 = _tc => {
const ydoc = new Y.Doc({ gc: false })
const updates = /** @type {Array<Uint8Array<ArrayBuffer>>} */ ([])
ydoc.on(enc.updateEventName, update => { updates.push(update) })
const array = ydoc.getArray()
const array = ydoc.get()
array.insert(0, [1, 2])
array.delete(1, 1)
array.insert(0, [3, 4])
@@ -274,7 +274,7 @@ export const testMergePendingUpdates = _tc => {
yDoc.on('update', (update, _origin, _c) => {
serverUpdates.splice(serverUpdates.length, 0, update)
})
const yText = yDoc.getText('textBlock')
const yText = yDoc.get('textBlock')
yText.applyDelta(delta.create().insert('r'))
yText.applyDelta(delta.create().insert('o'))
yText.applyDelta(delta.create().insert('n'))
@@ -305,7 +305,7 @@ export const testMergePendingUpdates = _tc => {
Y.applyUpdate(yDoc5, serverUpdates[4])
Y.encodeStateAsUpdate(yDoc5)
const yText5 = yDoc5.getText('textBlock')
const yText5 = yDoc5.get('textBlock')
t.compareStrings(yText5.toString(), 'nenor')
}
@@ -314,26 +314,26 @@ export const testMergePendingUpdates = _tc => {
*/
export const testObfuscateUpdates = _tc => {
const ydoc = new Y.Doc()
const ytext = ydoc.getText('text')
const ymap = ydoc.getMap('map')
const yarray = ydoc.getArray('array')
const ytext = ydoc.get('text')
const ymap = ydoc.get('map')
const yarray = ydoc.get('array')
// test ytext
ytext.applyDelta(delta.create().insert('text', { bold: true }).insert([{ href: 'supersecreturl' }]))
// test ymap
ymap.set('key', 'secret1')
ymap.set('key', 'secret2')
ymap.setAttr('key', 'secret1')
ymap.setAttr('key', 'secret2')
// test yarray with subtype & subdoc
const subtype = new Y.XmlElement('secretnodename')
const subtype = new Y.Type('secretnodename')
const subdoc = new Y.Doc({ guid: 'secret' })
subtype.setAttribute('attr', 'val')
subtype.setAttr('attr', 'val')
yarray.insert(0, ['teststring', 42, subtype, subdoc])
// obfuscate the content and put it into a new document
const obfuscatedUpdate = Y.obfuscateUpdate(Y.encodeStateAsUpdate(ydoc))
const odoc = new Y.Doc()
Y.applyUpdate(odoc, obfuscatedUpdate)
const otext = odoc.getText('text')
const omap = odoc.getMap('map')
const oarray = odoc.getArray('array')
const otext = odoc.get('text')
const omap = odoc.get('map')
const oarray = odoc.get('array')
// test ytext
const d = /** @type {any} */ (otext.getContent().toJSON().children)
t.assert(d.length === 2)
@@ -343,19 +343,19 @@ export const testObfuscateUpdates = _tc => {
t.assert(object.length(d[1].insert) === 1)
t.assert(object.hasProperty(d[1], 'insert'))
// test ymap
t.assert(omap.size === 1)
t.assert(!omap.has('key'))
t.assert(omap.attrSize === 1)
t.assert(!omap.hasAttr('key'))
// test yarray with subtype & subdoc
const result = oarray.toArray()
t.assert(result.length === 4)
t.assert(result[0] !== 'teststring')
t.assert(result[1] !== 42)
const osubtype = /** @type {Y.XmlElement} */ (result[2])
const osubtype = /** @type {Y.Type} */ (result[2])
const osubdoc = result[3]
// test subtype
t.assert(osubtype.nodeName !== subtype.nodeName)
t.assert(object.length(osubtype.getAttributes()) === 1)
t.assert(osubtype.getAttribute('attr') === undefined)
t.assert(osubtype.name !== subtype.name)
t.assert(object.length(osubtype.getAttrs()) === 1)
t.assert(osubtype.getAttr('attr') === undefined)
// test subdoc
t.assert(osubdoc.guid !== subdoc.guid)
}

View File

@@ -14,10 +14,10 @@ const isDevMode = env.getVariable('node_env') === 'development'
export const testBasicUpdate = _tc => {
const doc1 = new Y.Doc()
const doc2 = new Y.Doc()
doc1.getArray('array').insert(0, ['hi'])
doc1.get('array').insert(0, ['hi'])
const update = Y.encodeStateAsUpdate(doc1)
Y.applyUpdate(doc2, update)
t.compare(doc2.getArray('array').toArray(), ['hi'])
t.compare(doc2.get('array').toArray(), ['hi'])
}
/**
@@ -29,8 +29,8 @@ export const testFailsObjectManipulationInDevMode = _tc => {
const doc = new Y.Doc()
const a = [1, 2, 3]
const b = { o: 1 }
doc.getArray('test').insert(0, [a])
doc.getMap('map').set('k', b)
doc.get('test').insert(0, [a])
doc.get('map').setAttr('k', b)
t.fails(() => {
a[0] = 42
})
@@ -47,7 +47,7 @@ export const testFailsObjectManipulationInDevMode = _tc => {
*/
export const testSlice = _tc => {
const doc1 = new Y.Doc()
const arr = doc1.getArray('array')
const arr = doc1.get('array')
arr.insert(0, [1, 2, 3])
t.compareArrays(arr.slice(0), [1, 2, 3])
t.compareArrays(arr.slice(1), [2, 3])
@@ -62,9 +62,9 @@ export const testSlice = _tc => {
*/
export const testArrayFrom = _tc => {
const doc1 = new Y.Doc()
const db1 = doc1.getMap('root')
const nestedArray1 = Y.Array.from([0, 1, 2])
db1.set('array', nestedArray1)
const db1 = doc1.get('root')
const nestedArray1 = Y.Type.from(delta.create().insert([0, 1, 2]))
db1.setAttr('array', nestedArray1)
t.compare(nestedArray1.toArray(), [0, 1, 2])
}
@@ -75,7 +75,7 @@ export const testArrayFrom = _tc => {
*/
export const testLengthIssue = _tc => {
const doc1 = new Y.Doc()
const arr = doc1.getArray('array')
const arr = doc1.get('array')
arr.push([0, 1, 2, 3])
arr.delete(0)
arr.insert(0, [0])
@@ -104,7 +104,7 @@ export const testLengthIssue = _tc => {
*/
export const testLengthIssue2 = _tc => {
const doc = new Y.Doc()
const next = doc.getArray()
const next = doc.get()
doc.transact(() => {
next.insert(0, ['group2'])
})
@@ -162,9 +162,9 @@ export const testDeleteInsert = tc => {
export const testInsertThreeElementsTryRegetProperty = tc => {
const { testConnector, users, array0, array1 } = init(tc, { users: 2 })
array0.insert(0, [1, true, false])
t.compare(array0.toJSON(), [1, true, false], '.toJSON() works')
t.compare(array0.getContent(), delta.create().insert([1, true, false]), 'content works')
testConnector.flushAllMessages()
t.compare(array1.toJSON(), [1, true, false], '.toJSON() works after sync')
t.compare(array1.getContent(), delta.create().insert([1, true, false]), 'comparison works after sync')
compare(users)
}
@@ -222,8 +222,8 @@ export const testDisconnectReallyPreventsSendingMessages = tc => {
users[2].disconnect()
array0.insert(1, ['user0'])
array1.insert(1, ['user1'])
t.compare(array0.toJSON(), ['x', 'user0', 'y'])
t.compare(array1.toJSON(), ['x', 'user1', 'y'])
t.compare(array0.toJSON().children, ['x', 'user0', 'y'])
t.compare(array1.toJSON().children, ['x', 'user1', 'y'])
users[1].connect()
users[2].connect()
compare(users)
@@ -318,7 +318,7 @@ export const testInsertAndDeleteEventsForTypes = tc => {
array0.observe(e => {
event = e
})
array0.insert(0, [new Y.Array()])
array0.insert(0, [new Y.Type()])
t.assert(event !== null)
event = null
array0.delete(0)
@@ -327,34 +327,6 @@ export const testInsertAndDeleteEventsForTypes = tc => {
compare(users)
}
/**
* This issue has been reported in https://discuss.yjs.dev/t/order-in-which-events-yielded-by-observedeep-should-be-applied/261/2
*
* Deep observers generate multiple events. When an array added at item at, say, position 0,
* and item 1 changed then the array-add event should fire first so that the change event
* path is correct. A array binding might lead to an inconsistent state otherwise.
*
* @param {t.TestCase} tc
*/
export const testObserveDeepEventOrder = tc => {
const { array0, users } = init(tc, { users: 2 })
/**
* @type {Array<any>}
*/
let events = []
array0.observeDeep(e => {
events = e
})
array0.insert(0, [new Y.Map()])
users[0].transact(() => {
array0.get(0).set('a', 'a')
array0.insert(0, [0])
})
for (let i = 1; i < events.length; i++) {
t.assert(events[i - 1].path.length <= events[i].path.length, 'path size increases, fire top-level events first')
}
}
/**
* Correct index when computing event.path in observeDeep - https://github.com/yjs/yjs/issues/457
*
@@ -362,18 +334,18 @@ export const testObserveDeepEventOrder = tc => {
*/
export const testObservedeepIndexes = _tc => {
const doc = new Y.Doc()
const map = doc.getMap()
const map = doc.get()
// Create a field with the array as value
map.set('my-array', new Y.Array())
map.setAttr('my-array', new Y.Type())
// Fill the array with some strings and our Map
map.get('my-array').push(['a', 'b', 'c', new Y.Map()])
map.getAttr('my-array').push(['a', 'b', 'c', new Y.Type()])
/**
* @type {Array<any>}
*/
let eventPath = []
map.observeDeep((events) => { eventPath = events[0].path })
map.observeDeep((event) => { eventPath = event.path })
// set a value on the map inside of our array
map.get('my-array').get(3).set('hello', 'world')
map.getAttr('my-array').get(3).set('hello', 'world')
console.log(eventPath)
t.compare(eventPath, ['my-array', 3])
}
@@ -384,13 +356,13 @@ export const testObservedeepIndexes = _tc => {
export const testChangeEvent = tc => {
const { array0, users } = init(tc, { users: 2 })
/**
* @type {delta.Delta<any,any,any,any,any>}
* @type {delta.Delta<any>}
*/
let d = delta.create()
array0.observe(e => {
d = e.delta
})
const newArr = new Y.Array()
const newArr = new Y.Type()
array0.insert(0, [newArr, 4, 'dtrn'])
t.assert(d !== null && d.children.len === 1)
t.compare(d, delta.create().insert([newArr, 4, 'dtrn']))
@@ -415,7 +387,7 @@ export const testInsertAndDeleteEventsForTypes2 = tc => {
array0.observe(e => {
events.push(e)
})
array0.insert(0, ['hi', new Y.Map()])
array0.insert(0, ['hi', new Y.Type()])
t.assert(events.length === 1, 'Event is triggered exactly once for insertion of two elements')
array0.delete(1)
t.assert(events.length === 2, 'Event is triggered exactly once for deletion')
@@ -430,12 +402,12 @@ export const testNewChildDoesNotEmitEventInTransaction = tc => {
const { array0, users } = init(tc, { users: 2 })
let fired = false
users[0].transact(() => {
const newMap = new Y.Map()
const newMap = new Y.Type()
newMap.observe(() => {
fired = true
})
array0.insert(0, [newMap])
newMap.set('tst', 42)
newMap.setAttr('tst', 42)
})
t.assert(!fired, 'Event does not trigger')
}
@@ -494,15 +466,15 @@ export const testEventTargetIsSetCorrectlyOnRemote = tc => {
*/
export const testIteratingArrayContainingTypes = _tc => {
const y = new Y.Doc()
const arr = y.getArray('arr')
const arr = y.get('arr')
const numItems = 10
for (let i = 0; i < numItems; i++) {
const map = new Y.Map()
map.set('value', i)
const map = new Y.Type()
map.setAttr('value', i)
arr.push([map])
}
let cnt = 0
for (const item of arr) {
for (const item of arr.toArray()) {
t.assert(item.get('value') === cnt++, 'value is correct')
}
y.destroy()
@@ -514,9 +486,9 @@ export const testIteratingArrayContainingTypes = _tc => {
export const testAttributedContent = _tc => {
const ydoc = new Y.Doc({ gc: false })
/**
* @type {Y.Array<number>}
* @type {Y.Type<{ children: number }>}
*/
const yarray = ydoc.getArray()
const yarray = ydoc.get()
yarray.insert(0, [1, 2])
let attributionManager = Y.noAttributionsManager
@@ -544,7 +516,7 @@ const getUniqueNumber = () => _uniqueNumber++
*/
const arrayTransactions = [
function insert (user, gen) {
const yarray = user.getArray('array')
const yarray = user.get('array')
const uniqueNumber = getUniqueNumber()
const content = []
const len = prng.int32(gen, 1, 4)
@@ -558,35 +530,35 @@ const arrayTransactions = [
t.compareArrays(yarray.toArray(), oldContent) // we want to make sure that fastSearch markers insert at the correct position
},
function insertTypeArray (user, gen) {
const yarray = user.getArray('array')
const yarray = user.get('array')
const pos = prng.int32(gen, 0, yarray.length)
yarray.insert(pos, [new Y.Array()])
yarray.insert(pos, [new Y.Type()])
const array2 = yarray.get(pos)
array2.insert(0, [1, 2, 3, 4])
},
function insertTypeMap (user, gen) {
const yarray = user.getArray('array')
const yarray = user.get('array')
const pos = prng.int32(gen, 0, yarray.length)
yarray.insert(pos, [new Y.Map()])
yarray.insert(pos, [new Y.Type()])
const map = yarray.get(pos)
map.set('someprop', 42)
map.set('someprop', 43)
map.set('someprop', 44)
},
function insertTypeNull (user, gen) {
const yarray = user.getArray('array')
const yarray = user.get('array')
const pos = prng.int32(gen, 0, yarray.length)
yarray.insert(pos, [null])
},
function _delete (user, gen) {
const yarray = user.getArray('array')
const yarray = user.get('array')
const length = yarray.length
if (length > 0) {
let somePos = prng.int32(gen, 0, length - 1)
let delLength = prng.int32(gen, 1, math.min(2, length - somePos))
if (prng.bool(gen)) {
const type = yarray.get(somePos)
if (type instanceof Y.Array && type.length > 0) {
if (type instanceof Y.Type && type.length > 0) {
somePos = prng.int32(gen, 0, type.length - 1)
delLength = prng.int32(gen, 0, math.min(2, type.length - somePos))
type.delete(somePos, delLength)

View File

@@ -1,7 +1,6 @@
import * as Y from '../src/index.js'
import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line
import {
compareIDs,
noAttributionsManager,
TwosetAttributionManager,
createIdMapFromIdSet
@@ -18,35 +17,34 @@ import * as object from 'lib0/object'
export const testIterators = _tc => {
const ydoc = new Y.Doc()
/**
* @type {Y.Map<number>}
* @type {Y.Type<{attrs: { [k:string]: number} }>}
*/
const ymap = ydoc.getMap()
const ymap = ydoc.get()
// we are only checking if the type assumptions are correct
/**
* @type {Array<number>}
*/
const vals = Array.from(ymap.values())
const vals = Array.from(ymap.attrValues())
/**
* @type {Array<[string,number]>}
*/
const entries = Array.from(ymap.entries())
const entries = Array.from(ymap.attrEntries())
/**
* @type {Array<string>}
*/
const keys = Array.from(ymap.keys())
const keys = Array.from(ymap.attrKeys())
console.log(vals, entries, keys)
}
export const testNestedMapEvent = () => {
const ydoc = new Y.Doc()
const ymap = ydoc.getMap()
const ymapNested = ymap.set('nested', new Y.Map())
const ymap = ydoc.get()
const ymapNested = ymap.setAttr('nested', new Y.Type())
let called = 0
ymap.observeDeep((events, tr) => {
const event = events.find(event => event.target === ymap) || new Y.YEvent(ymap, tr, new Set())
ymap.observeDeep(event => {
const d = event.deltaDeep
called++
t.compare(d, delta.create().update('nested', delta.create().set('k', 'v')))
t.compare(d, delta.create().modifyAttr('nested', delta.create().setAttr('k', 'v')))
})
ymapNested.set('k', 'v')
t.assert(called === 1)
@@ -54,17 +52,16 @@ export const testNestedMapEvent = () => {
export const testNestedMapEvent2 = () => {
const ydoc = new Y.Doc()
const yarr = ydoc.getArray()
const ymapNested = new Y.Map()
const yarr = ydoc.get()
const ymapNested = new Y.Type()
yarr.insert(0, [ymapNested])
let called = 0
yarr.observeDeep((events, tr) => {
const event = events.find(event => event.target === yarr) || new Y.YEvent(yarr, tr, new Set())
yarr.observeDeep(event => {
const d = event.deltaDeep
called++
t.compare(d, delta.create().modify(delta.create().set('k', 'v')))
t.compare(d, delta.create().modify(delta.create().setAttr('k', 'v')))
})
ymapNested.set('k', 'v')
ymapNested.setAttr('k', 'v')
t.assert(called === 1)
}
@@ -75,7 +72,7 @@ export const testNestedMapEvent2 = () => {
*/
export const testMapEventError = _tc => {
const doc = new Y.Doc()
const ymap = doc.getMap()
const ymap = doc.get()
/**
* @type {any}
*/
@@ -96,26 +93,20 @@ export const testMapEventError = _tc => {
*/
export const testMapHavingIterableAsConstructorParamTests = tc => {
const { map0 } = init(tc, { users: 1 })
const m1 = new Y.Map(Object.entries({ number: 1, string: 'hello' }))
map0.set('m1', m1)
t.assert(m1.get('number') === 1)
t.assert(m1.get('string') === 'hello')
const m2 = new Y.Map([
['object', { x: 1 }],
['boolean', true]
])
map0.set('m2', m2)
t.assert(m2.get('object').x === 1)
t.assert(m2.get('boolean') === true)
const m3 = new Y.Map([...m1, ...m2])
map0.set('m3', m3)
t.assert(m3.get('number') === 1)
t.assert(m3.get('string') === 'hello')
t.assert(m3.get('object').x === 1)
t.assert(m3.get('boolean') === true)
const m1 = Y.Type.from(delta.create().setAttr('number', 1).setAttr('string', 'hello'))
map0.setAttr('m1', m1)
t.assert(m1.getAttr('number') === 1)
t.assert(m1.getAttr('string') === 'hello')
const m2 = Y.Type.from(delta.create(delta.$deltaAny).setAttrs({ object: { x: 1 }, boolean: true }).done())
map0.setAttr('m2', m2)
t.assert(m2.getAttr('object')?.x === 1)
t.assert(m2.getAttr('boolean') === true)
const m3 = new Y.Type().applyDelta(m1.getContent()).applyDelta(m2.getContent())
map0.setAttr('m3', m3)
t.assert(m3.getAttr('number') === 1)
t.assert(m3.getAttr('string') === 'hello')
t.assert(m3.getAttr('object')?.x === 1)
t.assert(m3.getAttr('boolean') === true)
}
/**
@@ -125,48 +116,48 @@ export const testBasicMapTests = tc => {
const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 })
users[2].disconnect()
map0.set('null', null)
map0.set('number', 1)
map0.set('string', 'hello Y')
map0.set('object', { key: { key2: 'value' } })
map0.set('y-map', new Y.Map())
map0.set('boolean1', true)
map0.set('boolean0', false)
const map = map0.get('y-map')
map.set('y-array', new Y.Array())
map0.setAttr('null', null)
map0.setAttr('number', 1)
map0.setAttr('string', 'hello Y')
map0.setAttr('object', { key: { key2: 'value' } })
map0.setAttr('y-map', new Y.Type())
map0.setAttr('boolean1', true)
map0.setAttr('boolean0', false)
const map = map0.getAttr('y-map')
map.set('y-array', new Y.Type())
const array = map.get('y-array')
array.insert(0, [0])
array.insert(0, [-1])
t.assert(map0.get('null') === null, 'client 0 computed the change (null)')
t.assert(map0.get('number') === 1, 'client 0 computed the change (number)')
t.assert(map0.get('string') === 'hello Y', 'client 0 computed the change (string)')
t.assert(map0.get('boolean0') === false, 'client 0 computed the change (boolean)')
t.assert(map0.get('boolean1') === true, 'client 0 computed the change (boolean)')
t.compare(map0.get('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)')
t.assert(map0.get('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)')
t.assert(map0.size === 7, 'client 0 map has correct size')
t.assert(map0.getAttr('null') === null, 'client 0 computed the change (null)')
t.assert(map0.getAttr('number') === 1, 'client 0 computed the change (number)')
t.assert(map0.getAttr('string') === 'hello Y', 'client 0 computed the change (string)')
t.assert(map0.getAttr('boolean0') === false, 'client 0 computed the change (boolean)')
t.assert(map0.getAttr('boolean1') === true, 'client 0 computed the change (boolean)')
t.compare(map0.getAttr('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)')
t.assert(map0.getAttr('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)')
t.assert(map0.attrSize === 7, 'client 0 map has correct size')
users[2].connect()
testConnector.flushAllMessages()
t.assert(map1.get('null') === null, 'client 1 received the update (null)')
t.assert(map1.get('number') === 1, 'client 1 received the update (number)')
t.assert(map1.get('string') === 'hello Y', 'client 1 received the update (string)')
t.assert(map1.get('boolean0') === false, 'client 1 computed the change (boolean)')
t.assert(map1.get('boolean1') === true, 'client 1 computed the change (boolean)')
t.compare(map1.get('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)')
t.assert(map1.get('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)')
t.assert(map1.size === 7, 'client 1 map has correct size')
t.assert(map1.getAttr('null') === null, 'client 1 received the update (null)')
t.assert(map1.getAttr('number') === 1, 'client 1 received the update (number)')
t.assert(map1.getAttr('string') === 'hello Y', 'client 1 received the update (string)')
t.assert(map1.getAttr('boolean0') === false, 'client 1 computed the change (boolean)')
t.assert(map1.getAttr('boolean1') === true, 'client 1 computed the change (boolean)')
t.compare(map1.getAttr('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)')
t.assert(map1.getAttr('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)')
t.assert(map1.attrSize === 7, 'client 1 map has correct size')
// compare disconnected user
t.assert(map2.get('null') === null, 'client 2 received the update (null) - was disconnected')
t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected')
t.assert(map2.get('string') === 'hello Y', 'client 2 received the update (string) - was disconnected')
t.assert(map2.get('boolean0') === false, 'client 2 computed the change (boolean)')
t.assert(map2.get('boolean1') === true, 'client 2 computed the change (boolean)')
t.compare(map2.get('object'), { key: { key2: 'value' } }, 'client 2 received the update (object) - was disconnected')
t.assert(map2.get('y-map').get('y-array').get(0) === -1, 'client 2 received the update (type) - was disconnected')
t.assert(map2.getAttr('null') === null, 'client 2 received the update (null) - was disconnected')
t.assert(map2.getAttr('number') === 1, 'client 2 received the update (number) - was disconnected')
t.assert(map2.getAttr('string') === 'hello Y', 'client 2 received the update (string) - was disconnected')
t.assert(map2.getAttr('boolean0') === false, 'client 2 computed the change (boolean)')
t.assert(map2.getAttr('boolean1') === true, 'client 2 computed the change (boolean)')
t.compare(map2.getAttr('object'), { key: { key2: 'value' } }, 'client 2 received the update (object) - was disconnected')
t.assert(map2.getAttr('y-map').get('y-array').get(0) === -1, 'client 2 received the update (type) - was disconnected')
compare(users)
}
@@ -175,18 +166,18 @@ export const testBasicMapTests = tc => {
*/
export const testGetAndSetOfMapProperty = tc => {
const { testConnector, users, map0 } = init(tc, { users: 2 })
map0.set('stuff', 'stuffy')
map0.set('undefined', undefined)
map0.set('null', null)
t.compare(map0.get('stuff'), 'stuffy')
map0.setAttr('stuff', 'stuffy')
map0.setAttr('undefined', undefined)
map0.setAttr('null', null)
t.compare(map0.getAttr('stuff'), 'stuffy')
testConnector.flushAllMessages()
for (const user of users) {
const u = user.getMap('map')
t.compare(u.get('stuff'), 'stuffy')
t.assert(u.get('undefined') === undefined, 'undefined')
t.compare(u.get('null'), null, 'null')
const u = user.get('map')
t.compare(u.getAttr('stuff'), 'stuffy')
t.assert(u.getAttr('undefined') === undefined, 'undefined')
t.compare(u.getAttr('null'), null, 'null')
}
compare(users)
}
@@ -196,8 +187,8 @@ export const testGetAndSetOfMapProperty = tc => {
*/
export const testYmapSetsYmap = tc => {
const { users, map0 } = init(tc, { users: 2 })
const map = map0.set('Map', new Y.Map())
t.assert(map0.get('Map') === map)
const map = map0.setAttr('Map', new Y.Type())
t.assert(map0.getAttr('Map') === map)
map.set('one', 1)
t.compare(map.get('one'), 1)
compare(users)
@@ -208,8 +199,8 @@ export const testYmapSetsYmap = tc => {
*/
export const testYmapSetsYarray = tc => {
const { users, map0 } = init(tc, { users: 2 })
const array = map0.set('Array', new Y.Array())
t.assert(array === map0.get('Array'))
const array = map0.setAttr('Array', new Y.Type())
t.assert(array === map0.getAttr('Array'))
array.insert(0, [1, 2, 3])
// @ts-ignore
t.compare(map0.toJSON(), { Array: [1, 2, 3] })
@@ -221,12 +212,12 @@ export const testYmapSetsYarray = tc => {
*/
export const testGetAndSetOfMapPropertySyncs = tc => {
const { testConnector, users, map0 } = init(tc, { users: 2 })
map0.set('stuff', 'stuffy')
t.compare(map0.get('stuff'), 'stuffy')
map0.setAttr('stuff', 'stuffy')
t.compare(map0.getAttr('stuff'), 'stuffy')
testConnector.flushAllMessages()
for (const user of users) {
const u = user.getMap('map')
t.compare(u.get('stuff'), 'stuffy')
const u = user.get('map')
t.compare(u.getAttr('stuff'), 'stuffy')
}
compare(users)
}
@@ -236,12 +227,12 @@ export const testGetAndSetOfMapPropertySyncs = tc => {
*/
export const testGetAndSetOfMapPropertyWithConflict = tc => {
const { testConnector, users, map0, map1 } = init(tc, { users: 3 })
map0.set('stuff', 'c0')
map1.set('stuff', 'c1')
map0.setAttr('stuff', 'c0')
map1.setAttr('stuff', 'c1')
testConnector.flushAllMessages()
for (const user of users) {
const u = user.getMap('map')
t.compare(u.get('stuff'), 'c1')
const u = user.get('map')
t.compare(u.getAttr('stuff'), 'c1')
}
compare(users)
}
@@ -251,13 +242,13 @@ export const testGetAndSetOfMapPropertyWithConflict = tc => {
*/
export const testSizeAndDeleteOfMapProperty = tc => {
const { map0 } = init(tc, { users: 1 })
map0.set('stuff', 'c0')
map0.set('otherstuff', 'c1')
t.assert(map0.size === 2, `map size is ${map0.size} expected 2`)
map0.delete('stuff')
t.assert(map0.size === 1, `map size after delete is ${map0.size}, expected 1`)
map0.delete('otherstuff')
t.assert(map0.size === 0, `map size after delete is ${map0.size}, expected 0`)
map0.setAttr('stuff', 'c0')
map0.setAttr('otherstuff', 'c1')
t.assert(map0.attrSize === 2, `map size is ${map0.attrSize} expected 2`)
map0.deleteAttr('stuff')
t.assert(map0.attrSize === 1, `map size after delete is ${map0.attrSize}, expected 1`)
map0.deleteAttr('otherstuff')
t.assert(map0.attrSize === 0, `map size after delete is ${map0.attrSize}, expected 0`)
}
/**
@@ -265,13 +256,13 @@ export const testSizeAndDeleteOfMapProperty = tc => {
*/
export const testGetAndSetAndDeleteOfMapProperty = tc => {
const { testConnector, users, map0, map1 } = init(tc, { users: 3 })
map0.set('stuff', 'c0')
map1.set('stuff', 'c1')
map1.delete('stuff')
map0.setAttr('stuff', 'c0')
map1.setAttr('stuff', 'c1')
map1.deleteAttr('stuff')
testConnector.flushAllMessages()
for (const user of users) {
const u = user.getMap('map')
t.assert(u.get('stuff') === undefined)
const u = user.get('map')
t.assert(u.getAttr('stuff') === undefined)
}
compare(users)
}
@@ -281,15 +272,15 @@ export const testGetAndSetAndDeleteOfMapProperty = tc => {
*/
export const testSetAndClearOfMapProperties = tc => {
const { testConnector, users, map0 } = init(tc, { users: 1 })
map0.set('stuff', 'c0')
map0.set('otherstuff', 'c1')
map0.clear()
map0.setAttr('stuff', 'c0')
map0.setAttr('otherstuff', 'c1')
map0.clearAttrs()
testConnector.flushAllMessages()
for (const user of users) {
const u = user.getMap('map')
t.assert(u.get('stuff') === undefined)
t.assert(u.get('otherstuff') === undefined)
t.assert(u.size === 0, `map size after clear is ${u.size}, expected 0`)
const u = user.get('map')
t.assert(u.getAttr('stuff') === undefined)
t.assert(u.getAttr('otherstuff') === undefined)
t.assert(u.attrSize === 0, `map size after clear is ${u.attrSize}, expected 0`)
}
compare(users)
}
@@ -299,22 +290,22 @@ export const testSetAndClearOfMapProperties = tc => {
*/
export const testSetAndClearOfMapPropertiesWithConflicts = tc => {
const { testConnector, users, map0, map1, map2, map3 } = init(tc, { users: 4 })
map0.set('stuff', 'c0')
map1.set('stuff', 'c1')
map1.set('stuff', 'c2')
map2.set('stuff', 'c3')
map0.setAttr('stuff', 'c0')
map1.setAttr('stuff', 'c1')
map1.setAttr('stuff', 'c2')
map2.setAttr('stuff', 'c3')
testConnector.flushAllMessages()
map0.set('otherstuff', 'c0')
map1.set('otherstuff', 'c1')
map2.set('otherstuff', 'c2')
map3.set('otherstuff', 'c3')
map3.clear()
map0.setAttr('otherstuff', 'c0')
map1.setAttr('otherstuff', 'c1')
map2.setAttr('otherstuff', 'c2')
map3.setAttr('otherstuff', 'c3')
map3.clearAttrs()
testConnector.flushAllMessages()
for (const user of users) {
const u = user.getMap('map')
t.assert(u.get('stuff') === undefined)
t.assert(u.get('otherstuff') === undefined)
t.assert(u.size === 0, `map size after clear is ${u.size}, expected 0`)
const u = user.get('map')
t.assert(u.getAttr('stuff') === undefined)
t.assert(u.getAttr('otherstuff') === undefined)
t.assert(u.attrSize === 0, `map size after clear is ${u.attrSize}, expected 0`)
}
compare(users)
}
@@ -324,14 +315,14 @@ export const testSetAndClearOfMapPropertiesWithConflicts = tc => {
*/
export const testGetAndSetOfMapPropertyWithThreeConflicts = tc => {
const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 })
map0.set('stuff', 'c0')
map1.set('stuff', 'c1')
map1.set('stuff', 'c2')
map2.set('stuff', 'c3')
map0.setAttr('stuff', 'c0')
map1.setAttr('stuff', 'c1')
map1.setAttr('stuff', 'c2')
map2.setAttr('stuff', 'c3')
testConnector.flushAllMessages()
for (const user of users) {
const u = user.getMap('map')
t.compare(u.get('stuff'), 'c3')
const u = user.get('map')
t.compare(u.getAttr('stuff'), 'c3')
}
compare(users)
}
@@ -341,114 +332,24 @@ export const testGetAndSetOfMapPropertyWithThreeConflicts = tc => {
*/
export const testGetAndSetAndDeleteOfMapPropertyWithThreeConflicts = tc => {
const { testConnector, users, map0, map1, map2, map3 } = init(tc, { users: 4 })
map0.set('stuff', 'c0')
map1.set('stuff', 'c1')
map1.set('stuff', 'c2')
map2.set('stuff', 'c3')
map0.setAttr('stuff', 'c0')
map1.setAttr('stuff', 'c1')
map1.setAttr('stuff', 'c2')
map2.setAttr('stuff', 'c3')
testConnector.flushAllMessages()
map0.set('stuff', 'deleteme')
map1.set('stuff', 'c1')
map2.set('stuff', 'c2')
map3.set('stuff', 'c3')
map3.delete('stuff')
map0.setAttr('stuff', 'deleteme')
map1.setAttr('stuff', 'c1')
map2.setAttr('stuff', 'c2')
map3.setAttr('stuff', 'c3')
map3.deleteAttr('stuff')
testConnector.flushAllMessages()
for (const user of users) {
const u = user.getMap('map')
t.assert(u.get('stuff') === undefined)
const u = user.get('map')
t.assert(u.getAttr('stuff') === undefined)
}
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testObserveDeepProperties = tc => {
const { testConnector, users, map1, map2, map3 } = init(tc, { users: 4 })
const _map1 = map1.set('map', new Y.Map())
let calls = 0
let dmapid
map1.observeDeep(events => {
events.forEach(event => {
calls++
// @ts-ignore
t.assert(event.keysChanged.has('deepmap'))
t.assert(event.path.length === 1)
t.assert(event.path[0] === 'map')
// @ts-ignore
dmapid = event.target.get('deepmap')._item.id
})
})
testConnector.flushAllMessages()
const _map3 = map3.get('map')
_map3.set('deepmap', new Y.Map())
testConnector.flushAllMessages()
const _map2 = map2.get('map')
_map2.set('deepmap', new Y.Map())
testConnector.flushAllMessages()
const dmap1 = _map1.get('deepmap')
const dmap2 = _map2.get('deepmap')
const dmap3 = _map3.get('deepmap')
t.assert(calls > 0)
t.assert(compareIDs(dmap1._item.id, dmap2._item.id))
t.assert(compareIDs(dmap1._item.id, dmap3._item.id))
// @ts-ignore we want the possibility of dmapid being undefined
t.assert(compareIDs(dmap1._item.id, dmapid))
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testObserversUsingObservedeep = tc => {
const { users, map0 } = init(tc, { users: 2 })
/**
* @type {Array<Array<string|number>>}
*/
const paths = []
let calls = 0
map0.observeDeep(events => {
events.forEach(event => {
paths.push(event.path)
})
calls++
})
map0.set('map', new Y.Map())
map0.get('map').set('array', new Y.Array())
map0.get('map').get('array').insert(0, ['content'])
t.assert(calls === 3)
t.compare(paths, [[], ['map'], ['map', 'array']])
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testPathsOfSiblingEvents = tc => {
const { users, map0 } = init(tc, { users: 2 })
/**
* @type {Array<Array<string|number>>}
*/
const paths = []
let calls = 0
const doc = users[0]
map0.set('map', new Y.Map())
map0.get('map').set('text1', new Y.Text('initial'))
map0.observeDeep(events => {
events.forEach(event => {
paths.push(event.path)
})
calls++
})
doc.transact(() => {
map0.get('map').get('text1').insert(0, 'post-')
map0.get('map').set('text2', new Y.Text('new'))
})
t.assert(calls === 1)
t.compare(paths, [['map'], ['map', 'text1']])
compare(users)
}
// TODO: Test events in Y.Map
/**
* @param {Object<string,any>} is
* @param {Object<string,any>} should
@@ -471,21 +372,21 @@ export const testThrowsAddAndUpdateAndDeleteEvents = tc => {
map0.observe(e => {
event = e // just put it on event, should be thrown synchronously anyway
})
map0.set('stuff', 4)
map0.setAttr('stuff', 4)
compareEvent(event, {
target: map0,
keysChanged: new Set(['stuff'])
})
// update, oldValue is in contents
map0.set('stuff', new Y.Array())
map0.setAttr('stuff', new Y.Type())
compareEvent(event, {
target: map0,
keysChanged: new Set(['stuff'])
})
// update, oldValue is in opContents
map0.set('stuff', 5)
map0.setAttr('stuff', 5)
// delete
map0.delete('stuff')
map0.deleteAttr('stuff')
compareEvent(event, {
keysChanged: new Set(['stuff']),
target: map0
@@ -506,10 +407,10 @@ export const testThrowsDeleteEventsOnClear = tc => {
event = e // just put it on event, should be thrown synchronously anyway
})
// set values
map0.set('stuff', 4)
map0.set('otherstuff', new Y.Array())
map0.setAttr('stuff', 4)
map0.setAttr('otherstuff', new Y.Type())
// clear
map0.clear()
map0.clearAttrs()
compareEvent(event, {
keysChanged: new Set(['stuff', 'otherstuff']),
target: map0
@@ -523,38 +424,38 @@ export const testThrowsDeleteEventsOnClear = tc => {
export const testChangeEvent = tc => {
const { map0, users } = init(tc, { users: 2 })
/**
* @type {delta.Delta<any,any,any,any>?}
* @type {delta.Delta<any>?}
*/
let changes = delta.create()
map0.observe(e => {
changes = e.delta
})
map0.set('a', 1)
map0.setAttr('a', 1)
let keyChange = changes.attrs.a
t.assert(delta.$insertOpWith(s.$number).check(keyChange) && keyChange.prevValue === undefined)
map0.set('a', 2)
map0.setAttr('a', 2)
keyChange = changes.attrs.a
t.assert(delta.$insertOpWith(s.$number).check(keyChange) && keyChange.prevValue === 1)
users[0].transact(() => {
map0.set('a', 3)
map0.set('a', 4)
map0.setAttr('a', 3)
map0.setAttr('a', 4)
})
keyChange = changes.attrs.a
t.assert(delta.$insertOpWith(s.$number).check(keyChange) && keyChange.prevValue === 2)
users[0].transact(() => {
map0.set('b', 1)
map0.set('b', 2)
map0.setAttr('b', 1)
map0.setAttr('b', 2)
})
keyChange = changes.attrs.b
t.assert(delta.$insertOpWith(s.$number).check(keyChange) && keyChange.prevValue === undefined)
users[0].transact(() => {
map0.set('c', 1)
map0.delete('c')
map0.setAttr('c', 1)
map0.deleteAttr('c')
})
t.assert(changes !== null && object.isEmpty(changes.attrs))
users[0].transact(() => {
map0.set('d', 1)
map0.set('d', 2)
map0.setAttr('d', 1)
map0.setAttr('d', 2)
})
keyChange = changes.attrs.d
t.assert(delta.$insertOpWith(s.$number).check(keyChange) && keyChange.prevValue === undefined)
@@ -566,7 +467,7 @@ export const testChangeEvent = tc => {
*/
export const testYmapEventExceptionsShouldCompleteTransaction = _tc => {
const doc = new Y.Doc()
const map = doc.getMap('map')
const map = doc.get('map')
let updateCalled = false
let throwingObserverCalled = false
@@ -589,7 +490,7 @@ export const testYmapEventExceptionsShouldCompleteTransaction = _tc => {
map.observeDeep(throwingDeepObserver)
t.fails(() => {
map.set('y', '2')
map.setAttr('y', '2')
})
t.assert(updateCalled)
@@ -601,14 +502,14 @@ export const testYmapEventExceptionsShouldCompleteTransaction = _tc => {
throwingObserverCalled = false
throwingDeepObserverCalled = false
t.fails(() => {
map.set('z', '3')
map.setAttr('z', '3')
})
t.assert(updateCalled)
t.assert(throwingObserverCalled)
t.assert(throwingDeepObserverCalled)
t.assert(map.get('z') === '3')
t.assert(map.getAttr('z') === '3')
}
/**
@@ -623,7 +524,7 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitive = tc => {
map0.observe(e => {
event = e
})
map0.set('stuff', 2)
map0.setAttr('stuff', 2)
t.compare(event.value, event.target.get(event.name))
compare(users)
}
@@ -640,7 +541,7 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc
map0.observe(e => {
event = e
})
map1.set('stuff', 2)
map1.setAttr('stuff', 2)
testConnector.flushAllMessages()
t.compare(event.value, event.target.get(event.name))
compare(users)
@@ -651,7 +552,7 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc
*/
export const testAttributedContent = _tc => {
const ydoc = new Y.Doc({ gc: false })
const ymap = ydoc.getMap()
const ymap = ydoc.get()
let attributionManager = noAttributionsManager
ydoc.on('afterTransaction', tr => {
@@ -659,21 +560,21 @@ export const testAttributedContent = _tc => {
attributionManager = new TwosetAttributionManager(createIdMapFromIdSet(tr.insertSet, []), createIdMapFromIdSet(tr.deleteSet, []))
})
t.group('initial value', () => {
ymap.set('test', 42)
ymap.setAttr('test', 42)
const expectedContent = { test: delta.$deltaMapChangeJson.expect({ type: 'insert', value: 42, attribution: { insert: [] } }) }
const attributedContent = ymap.getContent(attributionManager)
console.log(attributedContent.toJSON())
t.compare(expectedContent, attributedContent.toJSON().attrs)
})
t.group('overwrite value', () => {
ymap.set('test', 'fourtytwo')
ymap.setAttr('test', 'fourtytwo')
const expectedContent = { test: delta.$deltaMapChangeJson.expect({ type: 'insert', value: 'fourtytwo', attribution: { insert: [] } }) }
const attributedContent = ymap.getContent(attributionManager)
console.log(attributedContent)
t.compare(expectedContent, attributedContent.toJSON().attrs)
})
t.group('delete value', () => {
ymap.delete('test')
ymap.deleteAttr('test')
const expectedContent = { test: delta.$deltaMapChangeJson.expect({ type: 'delete', prevValue: 'fourtytwo', attribution: { delete: [] } }) }
const attributedContent = ymap.getContent(attributionManager)
console.log(attributedContent.toJSON())
@@ -688,21 +589,21 @@ const mapTransactions = [
function set (user, gen) {
const key = prng.oneOf(gen, ['one', 'two'])
const value = prng.utf16String(gen)
user.getMap('map').set(key, value)
user.get('map').setAttr(key, value)
},
function setType (user, gen) {
const key = prng.oneOf(gen, ['one', 'two'])
const type = prng.oneOf(gen, [new Y.Array(), new Y.Map()])
user.getMap('map').set(key, type)
if (type instanceof Y.Array) {
const type = new Y.Type()
user.get('map').setAttr(key, type)
if (prng.bool(gen)) {
type.insert(0, [1, 2, 3, 4])
} else {
type.set('deepkey', 'deepvalue')
type.setAttr('deepkey', 'deepvalue')
}
},
function _delete (user, gen) {
const key = prng.oneOf(gen, ['one', 'two'])
user.getMap('map').delete(key)
user.get('map').deleteAttr(key)
}
]

View File

@@ -148,7 +148,7 @@ export const testDeltaBug = _tc => {
'block-id': 'block-15630542-ef45-412d-9415-88f0052238ce'
})
const ydoc1 = new Y.Doc()
const ytext = ydoc1.getText()
const ytext = ydoc1.get()
ytext.applyDelta(initialDelta)
const addingDash = delta.create().retain(12).insert('-')
ytext.applyDelta(addingDash)
@@ -168,7 +168,7 @@ export const testDeltaBug = _tc => {
})
ytext.applyDelta(addingList)
const result = ytext.getContent()
const expectedResult = delta.text()
const expectedResult = delta.create()
.insert('\n', { 'block-id': 'block-28eea923-9cbb-4b6f-a950-cf7fd82bc087' })
.insert('\n\n\n', { 'table-col': { width: '150' } })
.insert('\n', {
@@ -306,7 +306,7 @@ export const testDeltaBug = _tc => {
* @param {t.TestCase} _tc
*/
export const testDeltaBug2 = _tc => {
const initialContent = delta.create()
const initialContent = delta.create(delta.$deltaAny)
.insert("Thomas' section")
.insert('\n', { 'block-id': 'block-61ae80ac-a469-4eae-bac9-3b6a2c380118' })
.insert('\n', { 'block-id': 'block-d265d93f-1cc7-40ee-bb58-8270fca2619f' })
@@ -1206,7 +1206,7 @@ export const testDeltaBug2 = _tc => {
})
.insert('\n', { 'block-id': 'block-21099df0-afb2-4cd3-834d-bb37800eb06a' })
const ydoc = new Y.Doc()
const ytext = ydoc.getText('id')
const ytext = ydoc.get('id')
ytext.applyDelta(initialContent)
const changeEvent = delta.create().retain(90).delete(4).retain(1, {
layout: null,
@@ -1350,7 +1350,7 @@ export const testFalsyFormats = tc => {
*/
export const testMultilineFormat = _tc => {
const ydoc = new Y.Doc()
const testText = ydoc.getText('test')
const testText = ydoc.get('test')
testText.insert(0, 'Test\nMulti-line\nFormatting')
const tt = delta.create()
.retain(4, { bold: true })
@@ -1376,7 +1376,7 @@ export const testMultilineFormat = _tc => {
*/
export const testNotMergeEmptyLinesFormat = _tc => {
const ydoc = new Y.Doc()
const testText = ydoc.getText('test')
const testText = ydoc.get('test')
testText.applyDelta(delta.create()
.insert('Text')
.insert('\n', { title: true })
@@ -1399,7 +1399,7 @@ export const testNotMergeEmptyLinesFormat = _tc => {
*/
export const testPreserveAttributesThroughDelete = _tc => {
const ydoc = new Y.Doc()
const testText = ydoc.getText('test')
const testText = ydoc.get('test')
testText.applyDelta(delta.create()
.insert('Text')
.insert('\n', { title: true })
@@ -1437,7 +1437,7 @@ export const testGetDeltaWithEmbeds = tc => {
export const testTypesAsEmbed = tc => {
const { text0, text1, testConnector } = init(tc, { users: 2 })
text0.applyDelta(delta.create()
.insert([new Y.Map([['key', 'val']])])
.insert([Y.Type.from(delta.create().setAttr('key', 'val'))])
)
t.compare(/** @type {any} */ (text0).getContentDeep().toJSON().children, [{ type: 'insert', insert: [{ type: 'delta', attrs: { key: { type: 'insert', value: 'val' } } }] }])
let firedEvent = false
@@ -1512,10 +1512,10 @@ export const testSnapshotDeleteAfter = tc => {
/**
* @param {t.TestCase} tc
*/
export const testToJson = tc => {
export const testDeltaCompare = tc => {
const { text0 } = init(tc, { users: 1 })
text0.insert(0, 'abc', { bold: true })
t.assert(text0.toJSON() === 'abc', 'toJSON returns the unformatted text')
t.compare(text0.getContent(), delta.create().insert('abc', { bold: true }).done())
}
/**
@@ -1524,7 +1524,7 @@ export const testToJson = tc => {
export const testToDeltaEmbedAttributes = tc => {
const { text0 } = init(tc, { users: 1 })
text0.insert(0, 'ab', { bold: true })
text0.insertEmbed(1, { image: 'imageSrc.png' }, { width: 100 })
text0.insert(1, [{ image: 'imageSrc.png' }], { width: 100 })
const delta0 = text0.getContent()
t.compare(
delta0,
@@ -1542,7 +1542,7 @@ export const testToDeltaEmbedAttributes = tc => {
export const testToDeltaEmbedNoAttributes = tc => {
const { text0 } = init(tc, { users: 1 })
text0.insert(0, 'ab', { bold: true })
text0.insertEmbed(1, { image: 'imageSrc.png' })
text0.insert(1, [{ image: 'imageSrc.png' }])
const delta0 = text0.getContent()
t.compare(
delta0,
@@ -1593,7 +1593,7 @@ export const testFormattingDeltaUnnecessaryAttributeChange = tc => {
})
testConnector.flushAllMessages()
/**
* @type {Array<delta.TextDelta<any>>}
* @type {Array<delta.Delta<any>>}
*/
const deltas = []
text0.observe(event => {
@@ -1706,7 +1706,7 @@ export const testLargeFragmentedDocument = _tc => {
let update = /** @type {any} */ (null)
;(() => {
const doc1 = new Y.Doc()
const text0 = doc1.getText('txt')
const text0 = doc1.get('txt')
tryGc()
t.measureTime(`time to insert ${itemsToInsert} items`, () => {
doc1.transact(() => {
@@ -1741,7 +1741,7 @@ export const testIncrementalUpdatesPerformanceOnLargeFragmentedDocument = _tc =>
doc1.on('update', update => {
updates.push(update)
})
const text0 = doc1.getText('txt')
const text0 = doc1.get('txt')
tryGc()
t.measureTime(`time to insert ${itemsToInsert} items`, () => {
doc1.transact(() => {
@@ -1846,13 +1846,13 @@ export const testSearchMarkerBug1 = tc => {
export const testFormattingBug = async _tc => {
const ydoc1 = new Y.Doc()
const ydoc2 = new Y.Doc()
const text1 = ydoc1.getText()
const text1 = ydoc1.get()
text1.insert(0, '\n\n\n')
text1.format(0, 3, { url: 'http://example.com' })
ydoc1.getText().format(1, 1, { url: 'http://docs.yjs.dev' })
ydoc2.getText().format(1, 1, { url: 'http://docs.yjs.dev' })
ydoc1.get().format(1, 1, { url: 'http://docs.yjs.dev' })
ydoc2.get().format(1, 1, { url: 'http://docs.yjs.dev' })
Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc1))
const text2 = ydoc2.getText()
const text2 = ydoc2.get()
const expectedResult = delta.create()
.insert('\n', { url: 'http://example.com' })
.insert('\n', { url: 'http://docs.yjs.dev' })
@@ -1870,11 +1870,11 @@ export const testFormattingBug = async _tc => {
*/
export const testDeleteFormatting = _tc => {
const doc = new Y.Doc()
const text = doc.getText()
const text = doc.get()
text.insert(0, 'Attack ships on fire off the shoulder of Orion.')
const doc2 = new Y.Doc()
const text2 = doc2.getText()
const text2 = doc2.get()
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
text.format(13, 7, { bold: true })
@@ -1897,7 +1897,7 @@ export const testDeleteFormatting = _tc => {
*/
export const testAttributedContent = _tc => {
const ydoc = new Y.Doc({ gc: false })
const ytext = ydoc.getText()
const ytext = ydoc.get()
ytext.insert(0, 'Hello World!')
let attributionManager = noAttributionsManager
@@ -1927,11 +1927,11 @@ export const testAttributedContent = _tc => {
export const testAttributedDiffing = _tc => {
const ydocVersion0 = new Y.Doc({ gc: false })
ydocVersion0.clientID = 0
ydocVersion0.getText().insert(0, 'Hello World!')
ydocVersion0.get().insert(0, 'Hello World!')
const ydoc = new Y.Doc({ gc: false })
ydoc.clientID = 1
Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(ydocVersion0))
const ytext = ydoc.getText()
const ytext = ydoc.get()
ytext.applyDelta(delta.create().retain(4, { italic: true }).retain(2).delete(5).insert('attributions'))
// this represents to all insertions of ydoc
const insertionSet = Y.createInsertionSetFromStructStore(ydoc.store, false)
@@ -1968,7 +1968,7 @@ const textChanges = [
* @param {prng.PRNG} gen
*/
(y, gen) => { // insert text
const ytext = y.getText('text')
const ytext = y.get('text')
const insertPos = prng.int32(gen, 0, ytext.length)
const text = charCounter++ + prng.word(gen)
const prevText = ytext.toString()
@@ -1980,7 +1980,7 @@ const textChanges = [
* @param {prng.PRNG} gen
*/
(y, gen) => { // delete text
const ytext = y.getText('text')
const ytext = y.get('text')
const contentLen = ytext.toString().length
const insertPos = prng.int32(gen, 0, contentLen)
const overwrite = math.min(prng.int32(gen, 0, contentLen - insertPos), 2)
@@ -2075,7 +2075,7 @@ const qChanges = [
* @param {prng.PRNG} gen
*/
(y, gen) => { // insert text
const ytext = y.getText('text')
const ytext = y.get('text')
const insertPos = prng.int32(gen, 0, ytext.length)
const attrs = prng.oneOf(gen, marksChoices)
const text = charCounter++ + prng.word(gen)
@@ -2086,12 +2086,12 @@ const qChanges = [
* @param {prng.PRNG} gen
*/
(y, gen) => { // insert embed
const ytext = y.getText('text')
const ytext = y.get('text')
const insertPos = prng.int32(gen, 0, ytext.length)
if (prng.bool(gen)) {
ytext.insertEmbed(insertPos, { image: 'https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png' })
ytext.insert(insertPos, [{ image: 'https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png' }])
} else {
ytext.insertEmbed(insertPos, new Y.Map([[prng.word(gen), prng.word(gen)]]))
ytext.insert(insertPos, [new Y.Type([[prng.word(gen), prng.word(gen)]])])
}
},
/**
@@ -2099,7 +2099,7 @@ const qChanges = [
* @param {prng.PRNG} gen
*/
(y, gen) => { // delete text
const ytext = y.getText('text')
const ytext = y.get('text')
const contentLen = ytext.toString().length
const insertPos = prng.int32(gen, 0, contentLen)
const overwrite = math.min(prng.int32(gen, 0, contentLen - insertPos), 2)
@@ -2110,7 +2110,7 @@ const qChanges = [
* @param {prng.PRNG} gen
*/
(y, gen) => { // format text
const ytext = y.getText('text')
const ytext = y.get('text')
const contentLen = ytext.toString().length
const insertPos = prng.int32(gen, 0, contentLen)
const overwrite = math.min(prng.int32(gen, 0, contentLen - insertPos), 2)
@@ -2122,7 +2122,7 @@ const qChanges = [
* @param {prng.PRNG} gen
*/
(y, gen) => { // insert codeblock
const ytext = y.getText('text')
const ytext = y.get('text')
const insertPos = prng.int32(gen, 0, ytext.toString().length)
const text = charCounter++ + prng.word(gen)
const d = delta.create()
@@ -2134,7 +2134,7 @@ const qChanges = [
* @param {prng.PRNG} gen
*/
(y, gen) => { // complex delta op
const ytext = y.getText('text')
const ytext = y.get('text')
const contentLen = ytext.toString().length
let currentPos = math.max(0, prng.int32(gen, 0, contentLen - 1))
const d = delta.create().retain(currentPos)
@@ -2191,7 +2191,7 @@ export const testAttributionManagerDefaultPerformance = tc => {
const MaxDeletionLength = 5 // 25% chance of deletion
const MaxInsertionLength = 5
const ydoc = new Y.Doc()
const ytext = ydoc.getText()
const ytext = ydoc.get()
for (let i = 0; i < N; i++) {
if (prng.bool(tc.prng) && prng.bool(tc.prng) && ytext.length > 0) {
const index = prng.int31(tc.prng, 0, ytext.length - 1)

View File

@@ -5,24 +5,24 @@ import * as delta from 'lib0/delta'
export const testCustomTypings = () => {
const ydoc = new Y.Doc()
const ymap = ydoc.getMap()
const ymap = ydoc.get()
/**
* @type {Y.XmlElement<{ num: number, str: string, [k:string]: object|number|string }>}
* @type {Y.Type<{ attrs: { num: number, str: string, [k:string]: number|string } }>}
*/
const yxml = ymap.set('yxml', new Y.XmlElement('test'))
const yxml = ymap.setAttr('yxml', new Y.Type('test'))
/**
* @type {number|undefined}
*/
const num = yxml.getAttribute('num')
const num = yxml.getAttr('num')
/**
* @type {string|undefined}
*/
const str = yxml.getAttribute('str')
const str = yxml.getAttr('str')
/**
* @type {object|number|string|undefined}
*/
const dtrn = yxml.getAttribute('dtrn')
const attrs = yxml.getAttributes()
const dtrn = yxml.getAttr('dtrn')
const attrs = yxml.getAttrs()
/**
* @type {object|number|string|undefined}
*/
@@ -35,10 +35,10 @@ export const testCustomTypings = () => {
*/
export const testSetProperty = tc => {
const { testConnector, users, xml0, xml1 } = init(tc, { users: 2 })
xml0.setAttribute('height', '10')
t.assert(xml0.getAttribute('height') === '10', 'Simple set+get works')
xml0.setAttr('height', '10')
t.assert(xml0.getAttr('height') === '10', 'Simple set+get works')
testConnector.flushAllMessages()
t.assert(xml1.getAttribute('height') === '10', 'Simple set+get works (remote)')
t.assert(xml1.getAttr('height') === '10', 'Simple set+get works (remote)')
compare(users)
}
@@ -47,15 +47,14 @@ export const testSetProperty = tc => {
*/
export const testHasProperty = tc => {
const { testConnector, users, xml0, xml1 } = init(tc, { users: 2 })
xml0.setAttribute('height', '10')
t.assert(xml0.hasAttribute('height'), 'Simple set+has works')
xml0.setAttr('height', '10')
t.assert(xml0.hasAttr('height'), 'Simple set+has works')
testConnector.flushAllMessages()
t.assert(xml1.hasAttribute('height'), 'Simple set+has works (remote)')
xml0.removeAttribute('height')
t.assert(!xml0.hasAttribute('height'), 'Simple set+remove+has works')
t.assert(xml1.hasAttr('height'), 'Simple set+has works (remote)')
xml0.deleteAttr('height')
t.assert(!xml0.hasAttr('height'), 'Simple set+remove+has works')
testConnector.flushAllMessages()
t.assert(!xml1.hasAttribute('height'), 'Simple set+remove+has works (remote)')
t.assert(!xml1.hasAttr('height'), 'Simple set+remove+has works (remote)')
compare(users)
}
@@ -64,13 +63,13 @@ export const testHasProperty = tc => {
*/
export const testYtextAttributes = _tc => {
const ydoc = new Y.Doc()
const ytext = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
const ytext = ydoc.get('')
ytext.observe(event => {
t.assert(event.delta.attrs.test?.type === 'insert')
})
ytext.setAttribute('test', 42)
t.compare(ytext.getAttribute('test'), 42)
t.compare(ytext.getAttributes(), { test: 42 })
ytext.setAttr('test', 42)
t.compare(ytext.getAttr('test'), 42)
t.compare(ytext.getAttrs(), { test: 42 })
}
/**
@@ -78,15 +77,12 @@ export const testYtextAttributes = _tc => {
*/
export const testSiblings = _tc => {
const ydoc = new Y.Doc()
const yxml = ydoc.getXmlFragment()
const first = new Y.XmlText()
const second = new Y.XmlElement('p')
const yxml = ydoc.get()
const first = new Y.Type()
const second = new Y.Type('p')
yxml.insert(0, [first, second])
t.assert(first.nextSibling === second)
t.assert(second.prevSibling === first)
t.assert(first.parent === /** @type {Y.AbstractType<any>} */ (yxml))
t.assert(yxml.parent === null)
t.assert(yxml.firstChild === first)
}
/**
@@ -94,13 +90,13 @@ export const testSiblings = _tc => {
*/
export const testInsertafter = _tc => {
const ydoc = new Y.Doc()
const yxml = ydoc.getXmlFragment()
const first = new Y.XmlText()
const second = new Y.XmlElement('p')
const third = new Y.XmlElement('p')
const yxml = ydoc.get()
const first = new Y.Type()
const second = new Y.Type('p')
const third = new Y.Type('p')
const deepsecond1 = new Y.XmlElement('span')
const deepsecond2 = new Y.XmlText()
const deepsecond1 = new Y.Type('span')
const deepsecond2 = new Y.Type()
second.insertAfter(null, [deepsecond1])
second.insertAfter(deepsecond1, [deepsecond2])
@@ -114,8 +110,8 @@ export const testInsertafter = _tc => {
t.compareArrays(yxml.toArray(), [first, second, third])
t.fails(() => {
const el = new Y.XmlElement('p')
el.insertAfter(deepsecond1, [new Y.XmlText()])
const el = new Y.Type('p')
el.insertAfter(deepsecond1, [new Y.Type()])
})
}
@@ -124,14 +120,14 @@ export const testInsertafter = _tc => {
*/
export const testClone = _tc => {
const ydoc = new Y.Doc()
const yxml = ydoc.getXmlFragment()
const first = new Y.XmlText('text')
const second = new Y.XmlElement('p')
const third = new Y.XmlElement('p')
const yxml = ydoc.get()
const first = new Y.Type('text')
const second = new Y.Type('p')
const third = new Y.Type('p')
yxml.push([first, second, third])
t.compareArrays(yxml.toArray(), [first, second, third])
const cloneYxml = yxml.clone()
ydoc.getArray('copyarr').insert(0, [cloneYxml])
ydoc.get('copyarr').insert(0, [cloneYxml])
t.assert(cloneYxml.length === 3)
t.compare(cloneYxml.toJSON(), yxml.toJSON())
}
@@ -141,7 +137,7 @@ export const testClone = _tc => {
*/
export const testFormattingBug = _tc => {
const ydoc = new Y.Doc()
const yxml = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
const yxml = ydoc.get()
const q = delta.create()
.insert('A', { em: {}, strong: {} })
.insert('B', { em: {} })
@@ -155,9 +151,9 @@ export const testFormattingBug = _tc => {
*/
export const testElement = _tc => {
const ydoc = new Y.Doc()
const yxmlel = ydoc.getXmlElement()
const text1 = new Y.XmlText('text1')
const text2 = new Y.XmlText('text2')
const yxmlel = ydoc.get()
const text1 = new Y.Type('text1')
const text2 = new Y.Type('text2')
yxmlel.insert(0, [text1, text2])
t.compareArrays(yxmlel.toArray(), [text1, text2])
}
@@ -167,12 +163,12 @@ export const testElement = _tc => {
*/
export const testFragmentAttributedContent = _tc => {
const ydoc = new Y.Doc({ gc: false })
const yfragment = new Y.XmlFragment()
const elem1 = new Y.XmlText('hello')
const elem2 = new Y.XmlElement()
const elem3 = new Y.XmlText('world')
const yfragment = new Y.Type()
const elem1 = new Y.Type('hello')
const elem2 = new Y.Type()
const elem3 = new Y.Type('world')
yfragment.insert(0, [elem1, elem2])
ydoc.getArray().insert(0, [yfragment])
ydoc.get().insert(0, [yfragment])
let attributionManager = Y.noAttributionsManager
ydoc.on('afterTransaction', tr => {
// attributionManager = new TwosetAttributionManager(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertedAt', 42), new Y.Attribution('insert', 'kevin')]), createIdMapFromIdSet(tr.deleteSet, [new Y.Attribution('delete', 'kevin')]))
@@ -196,10 +192,10 @@ export const testFragmentAttributedContent = _tc => {
*/
export const testElementAttributedContent = _tc => {
const ydoc = new Y.Doc({ gc: false })
const yelement = ydoc.getXmlElement('p')
const elem1 = new Y.XmlText('hello')
const elem2 = new Y.XmlElement('span')
const elem3 = new Y.XmlText('world')
const yelement = ydoc.get('p')
const elem1 = new Y.Type('hello')
const elem2 = new Y.Type('span')
const elem3 = new Y.Type('world')
yelement.insert(0, [elem1, elem2])
let attributionManager = Y.noAttributionsManager
ydoc.on('afterTransaction', tr => {
@@ -210,9 +206,9 @@ export const testElementAttributedContent = _tc => {
ydoc.transact(() => {
yelement.delete(0, 1)
yelement.insert(1, [elem3])
yelement.setAttribute('key', '42')
yelement.setAttr('key', '42')
})
const expectedContent = delta.create('UNDEFINED').insert([elem1], null, { delete: [] }).insert([elem2]).insert([elem3], null, { insert: [] }).set('key', '42', { insert: [] })
const expectedContent = delta.create('UNDEFINED').insert([elem1], null, { delete: [] }).insert([elem2]).insert([elem3], null, { insert: [] }).setAttr('key', '42', { insert: [] })
const attributedContent = yelement.getContent(attributionManager)
console.log('children', attributedContent.toJSON())
console.log('attributes', attributedContent)
@@ -221,15 +217,15 @@ export const testElementAttributedContent = _tc => {
t.group('test getContentDeep', () => {
const expectedContent = delta.create('UNDEFINED')
.insert(
[delta.text().insert('hello', null, { delete: [] })],
[delta.create().insert('hello', null, { delete: [] })],
null,
{ delete: [] }
)
.insert([delta.create('span')])
.insert([
delta.text().insert('world', null, { insert: [] })
delta.create().insert('world', null, { insert: [] })
], null, { insert: [] })
.set('key', '42', { insert: [] })
.setAttr('key', '42', { insert: [] })
.done()
const attributedContent = yelement.getContentDeep(attributionManager)
console.log('children', JSON.stringify(attributedContent.toJSON().children, null, 2))
@@ -247,19 +243,19 @@ export const testElementAttributedContent = _tc => {
*/
export const testElementAttributedContentViaDiffer = _tc => {
const ydocV1 = new Y.Doc()
ydocV1.getXmlElement('p').insert(0, [new Y.XmlText('hello'), new Y.XmlElement('span')])
ydocV1.get('p').insert(0, [new Y.Type('hello'), new Y.Type('span')])
const ydoc = new Y.Doc()
Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(ydocV1))
const yelement = ydoc.getXmlElement('p')
const yelement = ydoc.get('p')
const elem2 = yelement.get(1) // new Y.XmlElement('span')
const elem3 = new Y.XmlText('world')
const elem3 = new Y.Type('world')
ydoc.transact(() => {
yelement.delete(0, 1)
yelement.insert(1, [elem3])
yelement.setAttribute('key', '42')
yelement.setAttr('key', '42')
})
const attributionManager = Y.createAttributionManagerFromDiff(ydocV1, ydoc)
const expectedContent = delta.create('UNDEFINED').insert([delta.create().insert('hello')], null, { delete: [] }).insert([elem2.getContentDeep()]).insert([delta.create().insert('world', null, { insert: [] })], null, { insert: [] }).set('key', '42', { insert: [] })
const expectedContent = delta.create('UNDEFINED').insert([delta.create().insert('hello')], null, { delete: [] }).insert([elem2.getContentDeep()]).insert([delta.create().insert('world', null, { insert: [] })], null, { insert: [] }).setAttr('key', '42', { insert: [] })
const attributedContent = yelement.getContentDeep(attributionManager)
console.log('children', attributedContent.toJSON().children)
console.log('attributes', attributedContent.toJSON().attrs)
@@ -277,7 +273,7 @@ export const testElementAttributedContentViaDiffer = _tc => {
.insert([
delta.create().insert('world', null, { insert: [] })
], null, { insert: [] })
.set('key', '42', { insert: [] })
.setAttr('key', '42', { insert: [] })
const attributedContent = yelement.getContentDeep(attributionManager)
console.log('children', JSON.stringify(attributedContent.toJSON().children, null, 2))
console.log('cs expec', JSON.stringify(expectedContent.toJSON(), null, 2))
@@ -301,7 +297,7 @@ export const testElementAttributedContentViaDiffer = _tc => {
.insert([
delta.create().insert('bigworld', null, { insert: [] })
], null, { insert: [] })
.set('key', '42', { insert: [] })
.setAttr('key', '42', { insert: [] })
const attributedContent = yelement.getContentDeep(attributionManager)
console.log('children', JSON.stringify(attributedContent.toJSON().children, null, 2))
console.log('cs expec', JSON.stringify(expectedContent.toJSON(), null, 2))
@@ -315,7 +311,7 @@ export const testElementAttributedContentViaDiffer = _tc => {
t.info('expecting diffingAttributionManager to auto update itself')
const expectedContent = delta.create('UNDEFINED').insert([delta.create('span')]).insert([
delta.create().insert('bigworld')
]).set('key', '42')
]).setAttr('key', '42')
const attributedContent = yelement.getContentDeep(attributionManager)
console.log('children', JSON.stringify(attributedContent.toJSON().children, null, 2))
console.log('cs expec', JSON.stringify(expectedContent.toJSON(), null, 2))
@@ -333,21 +329,21 @@ export const testAttributionManagerSimpleExample = _tc => {
const ydoc = new Y.Doc()
ydoc.clientID = 0
// create some initial content
ydoc.getXmlFragment().insert(0, [new Y.XmlText('hello world')])
ydoc.get().insert(0, [new Y.Type('hello world')])
const ydocFork = new Y.Doc()
ydocFork.clientID = 1
Y.applyUpdate(ydocFork, Y.encodeStateAsUpdate(ydoc))
// modify the fork
// append a span element
ydocFork.getXmlFragment().insert(1, [new Y.XmlElement('span')])
const ytext = /** @type {Y.XmlText} */ (ydocFork.getXmlFragment().get(0))
ydocFork.get().insert(1, [new Y.Type('span')])
const ytext = ydocFork.get().get(0)
// make "hello" italic
ytext.format(0, 5, { italic: true })
ytext.insert(11, 'deleteme')
ytext.delete(11, 8)
ytext.insert(11, '!')
// highlight the changes
console.log(JSON.stringify(ydocFork.getXmlFragment().getContentDeep(Y.createAttributionManagerFromDiff(ydoc, ydocFork)), null, 2))
console.log(JSON.stringify(ydocFork.get().getContentDeep(Y.createAttributionManagerFromDiff(ydoc, ydocFork)), null, 2))
/* =>
{
"children": {