[wip] refactor for lib0/delta v2

This commit is contained in:
Kevin Jahns
2025-10-20 02:14:02 +02:00
parent 2f47a98380
commit 91384b54bf
35 changed files with 703 additions and 2743 deletions

View File

@@ -743,8 +743,6 @@ or any of its children.
<dd>Clone this type into a fresh Yjs type.</dd>
<b><code>toArray():Array&lt;Y.XmlElement|Y.XmlText&gt;</code></b>
<dd>Copies the children to a new Array.</dd>
<b><code>toDOM():DocumentFragment</code></b>
<dd>Transforms this type and all children to new DOM elements.</dd>
<b><code>toString():string</code></b>
<dd>Get the XML serialization of all descendants.</dd>
<b><code>toJSON():string</code></b>
@@ -818,8 +816,6 @@ content and be actually XML compliant.
<dd>Clone this type into a fresh Yjs type.</dd>
<b><code>toArray():Array&lt;Y.XmlElement|Y.XmlText&gt;</code></b>
<dd>Copies the children to a new Array.</dd>
<b><code>toDOM():Element</code></b>
<dd>Transforms this type and all children to a new DOM element.</dd>
<b><code>toString():string</code></b>
<dd>Get the XML serialization of all descendants.</dd>
<b><code>toJSON():string</code></b>

8
package-lock.json generated
View File

@@ -20,7 +20,7 @@
"rollup": "^4.37.0",
"standard": "^16.0.4",
"tui-jsdoc-template": "^1.2.2",
"typescript": "^5.8.3",
"typescript": "^5.9.3",
"yjs": "."
},
"engines": {
@@ -5292,9 +5292,9 @@
}
},
"node_modules/typescript": {
"version": "5.8.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
"integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"bin": {

View File

@@ -88,15 +88,15 @@
"lib0": "^0.2.114"
},
"devDependencies": {
"@y/protocols": "^1.0.6-1",
"@types/node": "^22.14.1",
"@y/protocols": "^1.0.6-1",
"concurrently": "^3.6.1",
"jsdoc": "^3.6.7",
"markdownlint-cli": "^0.41.0",
"rollup": "^4.37.0",
"standard": "^16.0.4",
"tui-jsdoc-template": "^1.2.2",
"typescript": "^5.8.3",
"typescript": "^5.9.3",
"yjs": "."
},
"engines": {

View File

@@ -10,10 +10,6 @@ export {
YXmlHook as XmlHook,
YXmlElement as XmlElement,
YXmlFragment as XmlFragment,
YXmlEvent,
YMapEvent,
YArrayEvent,
YTextEvent,
YEvent,
Item,
AbstractStruct,
@@ -74,7 +70,6 @@ export {
relativePositionToJSON,
isParentOf,
equalSnapshots,
PermanentUserData, // @TODO experimental
tryGc,
transact,
AbstractConnector,

View File

@@ -8,7 +8,6 @@ export * from './utils/EventHandler.js'
export * from './utils/ID.js'
export * from './utils/isParentOf.js'
export * from './utils/logging.js'
export * from './utils/PermanentUserData.js'
export * from './utils/RelativePosition.js'
export * from './utils/Snapshot.js'
export * from './utils/StructStore.js'
@@ -19,7 +18,6 @@ export * from './utils/YEvent.js'
export * from './utils/StructSet.js'
export * from './utils/IdMap.js'
export * from './utils/AttributionManager.js'
export * from './utils/Delta.js'
export * from './types/AbstractType.js'
export * from './types/YArray.js'
@@ -27,7 +25,6 @@ export * from './types/YMap.js'
export * from './types/YText.js'
export * from './types/YXmlFragment.js'
export * from './types/YXmlElement.js'
export * from './types/YXmlEvent.js'
export * from './types/YXmlHook.js'
export * from './types/YXmlText.js'

View File

@@ -6,13 +6,17 @@ import {
readYXmlFragment,
readYXmlHook,
readYXmlText,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, StructStore, Transaction, Item, AbstractType // eslint-disable-line
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<function(UpdateDecoderV1 | UpdateDecoderV2):AbstractType<any>>}
* @type {Array<(decoder: UpdateDecoderV1 | UpdateDecoderV2)=>(import('../utils/types.js').YType)>}
* @private
*/
export const typeRefs = [
@@ -38,11 +42,11 @@ export const YXmlTextRefID = 6
*/
export class ContentType {
/**
* @param {AbstractType<any>} type
* @param {YType_CT} type
*/
constructor (type) {
/**
* @type {AbstractType<any>}
* @type {YType_CT}
*/
this.type = type
}

View File

@@ -29,6 +29,10 @@ 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
*
@@ -68,7 +72,7 @@ export const followRedone = (store, id) => {
export const keepItem = (item, keep) => {
while (item !== null && item.keep !== keep) {
item.keep = keep
item = /** @type {AbstractType<any>} */ (item.parent)._item
item = /** @type {YType__} */ (item.parent)._item
}
}
@@ -115,7 +119,7 @@ export const splitItem = (transaction, leftItem, diff) => {
transaction._mergeStructs.push(rightItem)
// update parent._map
if (rightItem.parentSub !== null && rightItem.right === null) {
/** @type {AbstractType<any>} */ (rightItem.parent)._map.set(rightItem.parentSub, rightItem)
/** @type {YType__} */ (rightItem.parent)._map.set(rightItem.parentSub, rightItem)
}
} else {
rightItem.left = null
@@ -173,7 +177,7 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo
if (redone !== null) {
return getItemCleanStart(transaction, redone)
}
let parentItem = /** @type {AbstractType<any>} */ (item.parent)._item
let parentItem = /** @type {YType__} */ (item.parent)._item
/**
* @type {Item|null}
*/
@@ -192,7 +196,10 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo
parentItem = getItemCleanStart(transaction, parentItem.redone)
}
}
const parentType = parentItem === null ? /** @type {AbstractType<any>} */ (item.parent) : /** @type {ContentType} */ (parentItem.content).type
/**
* @type {YType__}
*/
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
@@ -205,10 +212,10 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo
*/
let leftTrace = left
// trace redone until parent matches
while (leftTrace !== null && /** @type {AbstractType<any>} */ (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 {AbstractType<any>} */ (leftTrace.parent)._item === parentItem) {
if (leftTrace !== null && /** @type {YType__} */ (leftTrace.parent)._item === parentItem) {
left = leftTrace
break
}
@@ -220,10 +227,10 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo
*/
let rightTrace = right
// trace redone until parent matches
while (rightTrace !== null && /** @type {AbstractType<any>} */ (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 {AbstractType<any>} */ (rightTrace.parent)._item === parentItem) {
if (rightTrace !== null && /** @type {YType__} */ (rightTrace.parent)._item === parentItem) {
right = rightTrace
break
}
@@ -275,7 +282,7 @@ export class Item extends AbstractStruct {
* @param {ID | null} origin
* @param {Item | null} right
* @param {ID | null} rightOrigin
* @param {AbstractType<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
*/
@@ -302,7 +309,7 @@ export class Item extends AbstractStruct {
*/
this.rightOrigin = rightOrigin
/**
* @type {AbstractType<any>|ID|null}
* @type {YType__|ID|null}
*/
this.parent = parent
/**
@@ -541,7 +548,7 @@ export class Item extends AbstractStruct {
addStruct(transaction.doc.store, this)
this.content.integrate(transaction, this)
// add parent to transaction.changed
addChangedTypeToTransaction(transaction, /** @type {AbstractType<any>} */ (this.parent), this.parentSub)
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)) {
// delete if parent is deleted or if this is not the current attribute value of parent
this.delete(transaction)
@@ -635,7 +642,7 @@ export class Item extends AbstractStruct {
*/
delete (transaction) {
if (!this.deleted) {
const parent = /** @type {AbstractType<any>} */ (this.parent)
const parent = /** @type {import('../utils/types.js').YType} */ (this.parent)
// adjust the length of parent
if (this.countable && this.parentSub === null) {
parent._length -= this.length

View File

@@ -8,18 +8,32 @@ import {
ContentType,
createID,
ContentAny,
ContentFormat,
ContentBinary,
ContentJSON,
ContentDeleted,
ContentString,
ContentEmbed,
getItemCleanStart,
noAttributionsManager,
ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, createAttributionFromAttributionItems, AbstractAttributionManager, // eslint-disable-line
} from '../internals.js'
import * as delta from '../utils/Delta.js'
import * as delta from 'lib0/delta'
import * as array from 'lib0/array'
import * as map from 'lib0/map'
import * as iterator from 'lib0/iterator'
import * as error from 'lib0/error'
import * as math from 'lib0/math'
import * as log from 'lib0/logging'
import * as object from 'lib0/object'
/**
* @typedef {import('../utils/types.js').YType} YType_
*/
/**
* @typedef {import('../utils/types.js').YValue} _YValue
*/
/**
* https://docs.yjs.dev/getting-started/working-with-shared-types#caveats
@@ -98,7 +112,7 @@ const markPosition = (searchMarker, p, index) => {
*
* This function always returns a refreshed marker (updated timestamp)
*
* @param {AbstractType<any>} yarray
* @param {import('../utils/types.js').YType} yarray
* @param {number} index
*/
export const findMarker = (yarray, index) => {
@@ -219,7 +233,7 @@ export const updateMarkerChanges = (searchMarker, index, len) => {
/**
* Accumulate all (list) children of a type and return them as an Array.
*
* @param {AbstractType<any>} t
* @param {AbstractType} t
* @return {Array<Item>}
*/
export const getTypeChildren = t => {
@@ -237,10 +251,9 @@ export const getTypeChildren = t => {
* Call event listeners with an event. This will also add an event to all
* parents (for `.observeDeep` handlers).
*
* @template EventType
* @param {AbstractType<EventType>} type
* @param {import('../utils/types.js').YType} type
* @param {Transaction} transaction
* @param {EventType} event
* @param {YEvent<any>} event
*/
export const callTypeObservers = (type, transaction, event) => {
const changedType = type
@@ -251,17 +264,15 @@ export const callTypeObservers = (type, transaction, event) => {
if (type._item === null) {
break
}
type = /** @type {AbstractType<any>} */ (type._item.parent)
type = /** @type {import('../utils/types.js').YType} */ (type._item.parent)
}
callEventHandlerListeners(changedType._eH, event, transaction)
callEventHandlerListeners(/** @type {any} */ (changedType._eH), event, transaction)
}
/**
* Abstract Yjs Type class
*
* @template EventType
* @template {import('../utils/Delta.js').Delta} [EventDelta=any]
* @template {import('../utils/Delta.js').Delta} [EventDeltaDeep=any]
* @template {delta.Delta<any,any,any,any,any>} [EventDelta=delta.Delta<any,any,any,any,any>]
* @template {YType_} [Self=any]
*/
export class AbstractType {
constructor () {
@@ -284,7 +295,7 @@ export class AbstractType {
this._length = 0
/**
* Event handlers
* @type {EventHandler<EventType,Transaction>}
* @type {EventHandler<YEvent<Self>,Transaction>}
*/
this._eH = createEventHandler()
/**
@@ -299,10 +310,18 @@ export class AbstractType {
}
/**
* @return {AbstractType<any,any>|null}
* Returns a fresh delta that can be used to change this YType.
* @type {EventDelta}
*/
get change () {
return /** @type {any} */ (delta.create())
}
/**
* @return {import('../utils/types.js').YType|null}
*/
get parent () {
return this._item ? /** @type {AbstractType<any,any>} */ (this._item.parent) : null
return /** @type {import('../utils/types.js').YType} */ (this._item ? this._item.parent : null)
}
/**
@@ -321,10 +340,11 @@ export class AbstractType {
}
/**
* @return {AbstractType<EventType,any>}
* @return {Self}
*/
_copy () {
throw error.methodUnimplemented()
// @ts-ignore
return new this.constructor()
}
/**
@@ -332,9 +352,10 @@ export class AbstractType {
*
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
*
* @return {AbstractType<EventType,any>}
* @return {Self}
*/
clone () {
// @todo remove this method from othern types by doing `_copy().apply(this.getContent())`
throw error.methodUnimplemented()
}
@@ -370,7 +391,7 @@ export class AbstractType {
/**
* Observe all events that are created on this type.
*
* @param {function(EventType, Transaction):void} f Observer function
* @param {(target: YEvent<Self>, tr: Transaction) => void} f Observer function
*/
observe (f) {
addEventHandlerListener(this._eH, f)
@@ -388,7 +409,7 @@ export class AbstractType {
/**
* Unregister an observer function.
*
* @param {function(EventType,Transaction):void} f Observer function
* @param {(type:YEvent<Self>,tr:Transaction)=>void} f Observer function
*/
unobserve (f) {
removeEventHandlerListener(this._eH, f)
@@ -410,24 +431,284 @@ export class AbstractType {
toJSON () {}
/**
* @param {AbstractAttributionManager} _am
* @return {EventDelta}
* 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
* @param {Object} [opts]
* @param {import('../utils/IdSet.js').IdSet?} [opts.itemsToRender]
* @param {boolean} [opts.retainInserts] - if true, retain rendered inserts with attributions
* @param {boolean} [opts.retainDeletes] - if true, retain rendered+attributed deletes only
* @param {Set<string>?} [opts.renderAttrs] - if true, retain rendered+attributed deletes only
* @param {boolean} [opts.renderChildren] - if true, retain rendered+attributed deletes only
* @return {EventDelta} The Delta representation of this type.
*
* @public
*/
getContent (_am) {
error.methodUnimplemented()
getContent (am = noAttributionsManager, { itemsToRender = null, retainInserts = false, retainDeletes = false, renderAttrs = null, renderChildren = true } = {}) {
/**
* @type {EventDelta}
*/
const d = /** @type {any} */ (delta.create())
typeMapGetDelta(d, /** @type {any} */ (this), renderAttrs, am)
if (renderChildren) {
/**
* @type {delta.FormattingAttributes}
*/
let currentAttributes = {} // saves all current attributes for insert
let usingCurrentAttributes = false
/**
* @type {delta.FormattingAttributes}
*/
let changedAttributes = {} // saves changed attributes for retain
let usingChangedAttributes = false
/**
* Logic for formatting attribute attribution
* Everything that comes after an formatting attribute is formatted by the user that created it.
* Two exceptions:
* - the user resets formatting to the previously known formatting that is not attributed
* - the user deletes a formatting attribute and hence restores the previously known formatting
* that is not attributed.
* @type {delta.FormattingAttributes}
*/
const previousUnattributedAttributes = {} // contains previously known unattributed formatting
/**
* @type {delta.FormattingAttributes}
*/
const previousAttributes = {} // The value before changes
/**
* @type {Array<import('../internals.js').AttributedContent<any>>}
*/
const cs = []
for (let item = this._start; item !== null; cs.length = 0) {
if (itemsToRender != null) {
for (; item !== null && cs.length < 50; item = item.right) {
const rslice = itemsToRender.slice(item.id.client, item.id.clock, item.length)
let itemContent = rslice.length > 1 ? item.content.copy() : item.content
for (let ir = 0; ir < rslice.length; ir++) {
const idrange = rslice[ir]
const content = itemContent
if (ir !== rslice.length - 1) {
itemContent = itemContent.splice(idrange.len)
}
am.readContent(cs, item.id.client, idrange.clock, item.deleted, content, idrange.exists ? 2 : 0)
}
}
} else {
for (; item !== null && cs.length < 50; item = item.right) {
am.readContent(cs, item.id.client, item.id.clock, item.deleted, item.content, 1)
}
}
for (let i = 0; i < cs.length; i++) {
const c = cs[i]
// render (attributed) content even if it was deleted
const renderContent = c.render && (!c.deleted || c.attrs != null)
// content that was just deleted. It is not rendered as an insertion, because it doesn't
// have any attributes.
const renderDelete = c.render && c.deleted
// existing content that should be retained, only adding changed attributes
const retainContent = !c.render && (!c.deleted || c.attrs != null)
const attribution = (renderContent || c.content.constructor === ContentFormat) ? createAttributionFromAttributionItems(c.attrs, c.deleted) : null
switch (c.content.constructor) {
case ContentDeleted: {
if (renderDelete) d.delete(c.content.getLength())
break
}
case ContentString:
if (renderContent) {
d.usedAttributes = currentAttributes
usingCurrentAttributes = true
if (c.deleted ? retainDeletes : retainInserts) {
d.retain(/** @type {ContentString} */ (c.content).str.length, null, attribution ?? {})
} else {
d.insert(/** @type {ContentString} */ (c.content).str, null, attribution)
}
} else if (renderDelete) {
d.delete(c.content.getLength())
} else if (retainContent) {
d.usedAttributes = changedAttributes
usingChangedAttributes = true
d.retain(c.content.getLength())
}
break
case ContentEmbed:
case ContentAny:
case ContentJSON:
case ContentType:
case ContentBinary:
if (renderContent) {
d.usedAttributes = currentAttributes
usingCurrentAttributes = true
if (c.deleted ? retainDeletes : retainInserts) {
d.retain(c.content.getLength(), null, attribution ?? {})
} else {
d.insert(c.content.getContent(), null, attribution)
}
} else if (renderDelete) {
d.delete(1)
} else if (retainContent) {
d.usedAttributes = changedAttributes
usingChangedAttributes = true
d.retain(1)
}
break
case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (c.content)
const currAttrVal = currentAttributes[key] ?? null
if (attribution != null && (c.deleted || !object.hasProperty(previousUnattributedAttributes, key))) {
previousUnattributedAttributes[key] = c.deleted ? value : currAttrVal
}
// @todo write a function "updateCurrentAttributes" and "updateChangedAttributes"
// # Update Attributes
if (renderContent || renderDelete) {
// create fresh references
if (usingCurrentAttributes) {
currentAttributes = object.assign({}, currentAttributes)
usingCurrentAttributes = false
}
if (usingChangedAttributes) {
usingChangedAttributes = false
changedAttributes = object.assign({}, changedAttributes)
}
}
if (renderContent || renderDelete) {
if (c.deleted) {
// content was deleted, but is possibly attributed
if (!equalAttrs(value, currAttrVal)) { // do nothing if nothing changed
if (equalAttrs(currAttrVal, previousAttributes[key] ?? null) && changedAttributes[key] !== undefined) {
delete changedAttributes[key]
} else {
changedAttributes[key] = currAttrVal
}
// current attributes doesn't change
previousAttributes[key] = value
}
} else { // !c.deleted
// content was inserted, and is possibly attributed
if (equalAttrs(value, currAttrVal)) {
// item.delete(transaction)
} else if (equalAttrs(value, previousAttributes[key] ?? null)) {
delete changedAttributes[key]
} else {
changedAttributes[key] = value
}
if (value == null) {
delete currentAttributes[key]
} else {
currentAttributes[key] = value
}
}
} else if (retainContent && !c.deleted) {
// fresh reference to currentAttributes only
if (usingCurrentAttributes) {
currentAttributes = object.assign({}, currentAttributes)
usingCurrentAttributes = false
}
if (usingChangedAttributes && changedAttributes[key] !== undefined) {
usingChangedAttributes = false
changedAttributes = object.assign({}, changedAttributes)
}
if (value == null) {
delete currentAttributes[key]
} else {
currentAttributes[key] = value
}
delete changedAttributes[key]
previousAttributes[key] = value
}
// # Update Attributions
if (attribution != null || object.hasProperty(previousUnattributedAttributes, key)) {
/**
* @type {import('../utils/AttributionManager.js').Attribution}
*/
const formattingAttribution = object.assign({}, d.usedAttribution)
const changedAttributedAttributes = /** @type {{ [key: string]: Array<any> }} */ (formattingAttribution.attributes = object.assign({}, formattingAttribution.attributes ?? {}))
if (attribution == null || equalAttrs(previousUnattributedAttributes[key], currentAttributes[key] ?? null)) {
// an unattributed formatting attribute was found or an attributed formatting
// attribute was found that resets to the previous status
delete changedAttributedAttributes[key]
delete previousUnattributedAttributes[key]
} else {
const by = changedAttributedAttributes[key] = (changedAttributedAttributes[key]?.slice() ?? [])
by.push(...((c.deleted ? attribution.delete : attribution.insert) ?? []))
const attributedAt = (c.deleted ? attribution.deletedAt : attribution.insertedAt)
if (attributedAt) formattingAttribution.attributedAt = attributedAt
}
if (object.isEmpty(changedAttributedAttributes)) {
d.useAttribution(null)
} else if (attribution != null) {
const attributedAt = (c.deleted ? attribution.deletedAt : attribution.insertedAt)
if (attributedAt != null) formattingAttribution.attributedAt = attributedAt
d.useAttribution(formattingAttribution)
}
}
break
}
}
}
}
}
return d
}
/**
* @param {AbstractAttributionManager} _am
* @return {EventDeltaDeep}
* Render the difference to another ydoc (which can be empty) and highlight the differences with
* attributions.
*
* @param {AbstractAttributionManager} am
* @return {ToDeepEventDelta<EventDelta>}
*/
getContentDeep (_am) {
error.methodUnimplemented()
getContentDeep (am = noAttributionsManager) {
const d = this.getContent(am)
d.children.forEach(op => {
if (op instanceof delta.InsertOp) {
op.insert = /** @type {any} */ (op.insert.map(ins =>
ins instanceof AbstractType
// @ts-ignore
? ins.getContentDeep(am)
: ins)
)
}
})
d.attrs.forEach((op) => {
if (delta.$insertOp.check(op) && op.value instanceof AbstractType) {
op.value = op.value.getContentDeep(am)
}
})
return /** @type {any} */ (d)
}
}
/**
* @param {AbstractType<any,any>} type
* @param {any} a
* @param {any} b
* @return {boolean}
*/
export const equalAttrs = (a, b) => a === b || (typeof a === 'object' && typeof b === 'object' && a && b && object.equalFlat(a, b))
/**
* @template {delta.Delta<any,any,any,any,any>} D
* @typedef {D extends delta.Delta<infer N,infer Attrs,infer Cs,infer Text,any>
* ? delta.Delta<
* N,
* { [K in keyof Attrs]: TypeToDelta<Attrs[K]> },
* TypeToDelta<Cs>,
* Text
* >
* : D
* } ToDeepEventDelta
*/
/**
* @template {any} T
* @typedef {(Extract<T,AbstractType<any>> extends AbstractType<infer D> ? (unknown extends D ? never : ToDeepEventDelta<D>) : never) | Exclude<T,AbstractType<any>>} TypeToDelta
*/
/**
* @param {AbstractType<any>} type
* @param {number} start
* @param {number} end
* @return {Array<any>}
@@ -465,7 +746,7 @@ export const typeListSlice = (type, start, end) => {
}
/**
* @param {AbstractType<any,any>} type
* @param {import('../utils/types.js').YType} type
* @return {Array<any>}
*
* @private
@@ -488,23 +769,23 @@ export const typeListToArray = type => {
}
/**
* @todo this can be removed as this can be replaced by a generic function
* 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, .. }`.
*
* @template {delta.ArrayDelta<any,any>} TypeDelta
* @param {AbstractType<any,TypeDelta,any>} type
* @template {delta.Delta<any,any,any,any>} TypeDelta
* @param {TypeDelta} d
* @param {import('../utils/types.js').YType} type
* @param {import('../internals.js').AbstractAttributionManager} am
* @return {TypeDelta}
*
* @private
* @function
*/
export const typeListGetContent = (type, am) => {
export const typeListGetContent = (d, type, am) => {
type.doc ?? warnPrematureAccess()
const d = delta.createArrayDelta()
/**
* @type {Array<import('../internals.js').AttributedContent<any>>}
*/
@@ -526,11 +807,10 @@ export const typeListGetContent = (type, am) => {
}
}
}
return /** @type {TypeDelta} */ (d.done())
}
/**
* @param {AbstractType<any,any>} type
* @param {AbstractType<any>} type
* @param {Snapshot} snapshot
* @return {Array<any>}
*
@@ -555,7 +835,7 @@ export const typeListToArraySnapshot = (type, snapshot) => {
/**
* Executes a provided function on once on every element of this YArray.
*
* @param {AbstractType<any,any>} type
* @param {AbstractType<any>} type
* @param {function(any,number,any):void} f A function to execute on every element of this YArray.
*
* @private
@@ -578,8 +858,8 @@ export const typeListForEach = (type, f) => {
/**
* @template C,R
* @param {AbstractType<any,any>} type
* @param {function(C,number,AbstractType<any,any>):R} f
* @param {AbstractType<any>} type
* @param {function(C,number,AbstractType<any>):R} f
* @return {Array<R>}
*
* @private
@@ -597,7 +877,7 @@ export const typeListMap = (type, f) => {
}
/**
* @param {AbstractType<any,any>} type
* @param {AbstractType} type
* @return {IterableIterator<any>}
*
* @private
@@ -649,8 +929,8 @@ export const typeListCreateIterator = type => {
* Executes a provided function on once on every element of this YArray.
* Operates on a snapshotted state of the document.
*
* @param {AbstractType<any,any>} type
* @param {function(any,number,AbstractType<any,any>):void} f A function to execute on every element of this YArray.
* @param {AbstractType} type
* @param {function(any,number,AbstractType):void} f A function to execute on every element of this YArray.
* @param {Snapshot} snapshot
*
* @private
@@ -671,7 +951,7 @@ export const typeListForEachSnapshot = (type, f, snapshot) => {
}
/**
* @param {AbstractType<any,any>} type
* @param {import('../utils/types.js').YType} type
* @param {number} index
* @return {any}
*
@@ -698,9 +978,9 @@ export const typeListGet = (type, index) => {
/**
* @param {Transaction} transaction
* @param {AbstractType<any,any>} parent
* @param {YType_} parent
* @param {Item?} referenceItem
* @param {Array<Object<string,any>|Array<any>|boolean|number|null|string|Uint8Array>} content
* @param {Array<_YValue>} content
*
* @private
* @function
@@ -750,7 +1030,7 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
break
default:
if (c instanceof AbstractType) {
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(c))
left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentType(/** @type {any} */ (c)))
left.integrate(transaction, 0)
} else {
throw new Error('Unexpected content type in insert operation')
@@ -766,7 +1046,7 @@ const lengthExceeded = () => error.create('Length exceeded!')
/**
* @param {Transaction} transaction
* @param {AbstractType<any,any>} parent
* @param {YType_} parent
* @param {number} index
* @param {Array<Object<string,any>|Array<any>|number|null|string|Uint8Array>} content
*
@@ -819,7 +1099,7 @@ export const typeListInsertGenerics = (transaction, parent, index, content) => {
* the search marker.
*
* @param {Transaction} transaction
* @param {AbstractType<any,any>} parent
* @param {YType_} parent
* @param {Array<Object<string,any>|Array<any>|number|null|string|Uint8Array>} content
*
* @private
@@ -839,7 +1119,7 @@ export const typeListPushGenerics = (transaction, parent, content) => {
/**
* @param {Transaction} transaction
* @param {AbstractType<any,any>} parent
* @param {import('../utils/types.js').YType} parent
* @param {number} index
* @param {number} length
*
@@ -886,7 +1166,7 @@ export const typeListDelete = (transaction, parent, index, length) => {
/**
* @param {Transaction} transaction
* @param {AbstractType<any,any>} parent
* @param {YType_} parent
* @param {string} key
*
* @private
@@ -901,9 +1181,9 @@ export const typeMapDelete = (transaction, parent, key) => {
/**
* @param {Transaction} transaction
* @param {AbstractType<any,any>} parent
* @param {YType_} parent
* @param {string} key
* @param {Object|number|null|Array<any>|string|Uint8Array|AbstractType<any,any>} value
* @param {_YValue} value
*
* @private
* @function
@@ -934,7 +1214,7 @@ export const typeMapSet = (transaction, parent, key, value) => {
break
default:
if (value instanceof AbstractType) {
content = new ContentType(value)
content = new ContentType(/** @type {any} */ (value))
} else {
throw new Error('Unexpected content type')
}
@@ -944,7 +1224,7 @@ export const typeMapSet = (transaction, parent, key, value) => {
}
/**
* @param {AbstractType<any>} parent
* @param {YType_} parent
* @param {string} key
* @return {Object<string,any>|number|null|Array<any>|string|Uint8Array|AbstractType<any>|undefined}
*
@@ -985,17 +1265,23 @@ export const typeMapGetAll = (parent) => {
* Note that deleted content that was not deleted in prevYdoc is rendered as an insertion with the
* attribution `{ isDeleted: true, .. }`.
*
* @template MapType
* @param {AbstractType<any>} parent
* @template {delta.Delta<any,any,any,any>} TypeDelta
* @param {TypeDelta} d
* @param {YType_} parent
* @param {Set<string>?} attrsToRender
* @param {import('../internals.js').AbstractAttributionManager} am
*
* @private
* @function
*/
export const typeMapGetDelta = (parent, am) => {
const mapdelta = /** @type {delta.MapDeltaBuilder<{ [key:string]: MapType }>} */ (delta.createMapDelta())
export const typeMapGetDelta = (d, parent, attrsToRender, am) => {
parent.doc ?? warnPrematureAccess()
parent._map.forEach((item, key) => {
/**
* @param {Item} item
* @param {string} key
*/
const renderAttrs = (item, key) => {
/**
* @type {Array<import('../internals.js').AttributedContent<any>>}
*/
@@ -1005,7 +1291,7 @@ export const typeMapGetDelta = (parent, am) => {
const c = array.last(content.getContent())
const attribution = createAttributionFromAttributionItems(attrs, deleted)
if (deleted) {
mapdelta.delete(key, c, attribution)
d.unset(key, attribution, c)
} else {
/**
* @type {Array<import('../internals.js').AttributedContent<any>>}
@@ -1027,10 +1313,14 @@ export const typeMapGetDelta = (parent, am) => {
}
}
const prevValue = cs.length > 0 ? array.last(cs[0].content.getContent()) : undefined
mapdelta.set(key, c, prevValue, attribution)
d.set(key, c, attribution, prevValue)
}
})
return mapdelta
}
if (attrsToRender == null) {
parent._map.forEach(renderAttrs)
} else {
attrsToRender.forEach(key => renderAttrs(/** @type {Item} */ (parent._map.get(key)), key))
}
}
/**

View File

@@ -23,28 +23,12 @@ import {
AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
} from '../internals.js'
/**
*
* @template Content
* @template {import('../internals.js').Delta|undefined} Modifiers
* @typedef {import('../internals.js').ArrayDelta<Content,Modifiers>} ArrayDelta
*/
import * as delta from '../utils/Delta.js'
/**
* Event that describes the changes on a YArray
* @template {import('../utils/types.js').YValue} T
* @extends YEvent<YArray<T>>
*/
export class YArrayEvent extends YEvent {}
import * as delta from 'lib0/delta' // eslint-disable-line
/**
* A shared Array implementation.
* @template {import('../utils/types.js').YValue} T
* @template {ArrayDelta<T,undefined>} [TypeDelta=ArrayDelta<T,undefined>]
* @template {T extends AbstractType<any,any,infer DeepD> ? ArrayDelta<Exclude<T,AbstractType<any>>|DeepD,DeepD> : ArrayDelta<T,undefined>} [EventDeltaDeep=T extends AbstractType<any,any,infer DeepD> ? ArrayDelta<Exclude<T,AbstractType<any>>|DeepD,DeepD> : ArrayDelta<T,undefined>]
* @extends AbstractType<YArrayEvent<T>,TypeDelta,EventDeltaDeep>
* @extends {AbstractType<delta.ArrayDelta<T>,YArray<T>>}
* @implements {Iterable<T>}
*/
export class YArray extends AbstractType {
@@ -84,7 +68,7 @@ export class YArray extends AbstractType {
* * Observer functions are fired
*
* @param {Doc} y The Yjs instance
* @param {Item} item
* @param {Item?} item
*/
_integrate (y, item) {
super._integrate(y, item)
@@ -92,13 +76,6 @@ export class YArray extends AbstractType {
this._prelimContent = null
}
/**
* @return {YArray<T>}
*/
_copy () {
return new YArray()
}
/**
* Makes a copy of this data type that can be included somewhere else.
*
@@ -108,11 +85,12 @@ export class YArray extends AbstractType {
*/
clone () {
/**
* @type {YArray<T>}
* @type {this}
*/
const arr = new YArray()
const arr = /** @type {this} */ (new YArray())
arr.insert(0, this.toArray().map(el =>
el instanceof AbstractType ? /** @type {typeof el} */ (el.clone()) : el
// @ts-ignore
el instanceof AbstractType ? /** @type {any} */ (el.clone()) : el
))
return arr
}
@@ -130,7 +108,7 @@ export class YArray extends AbstractType {
*/
_callObserver (transaction, parentSubs) {
super._callObserver(transaction, parentSubs)
callTypeObservers(this, transaction, new YArrayEvent(this, transaction))
callTypeObservers(this, transaction, new YEvent(this, transaction, null))
}
/**
@@ -228,16 +206,12 @@ export class YArray extends AbstractType {
* attribution `{ isDeleted: true, .. }`.
*
* @param {AbstractAttributionManager} am
* @return {EventDeltaDeep} The Delta representation of this type.
* @return {delta.ArrayDelta<import('./AbstractType.js').TypeToDelta<T>>} The Delta representation of this type.
*
* @public
*/
getContentDeep (am = noAttributionsManager) {
return /** @type {any} */ (this.getContent(am).map(d => /** @type {any} */ (
d instanceof delta.InsertArrayOp && d.insert instanceof Array
? new delta.InsertArrayOp(d.insert.map(e => e instanceof AbstractType ? e.getContentDeep(am) : e), d.attributes, d.attribution)
: d
)))
return super.getContentDeep(am)
}
/**
@@ -248,12 +222,14 @@ export class YArray extends AbstractType {
* attribution `{ isDeleted: true, .. }`.
*
* @param {AbstractAttributionManager} am
* @return {TypeDelta} The Delta representation of this type.
* @return {delta.ArrayDelta<T>} The Delta representation of this type.
*
* @public
*/
getContent (am = noAttributionsManager) {
return typeListGetContent(this, am)
const d = this.change
typeListGetContent(d, this, am)
return d
}
/**
@@ -316,6 +292,7 @@ export class YArray extends AbstractType {
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
* @return {import('../utils/types.js').YType}
*
* @private
* @function

View File

@@ -13,35 +13,18 @@ import {
YMapRefID,
callTypeObservers,
transact,
typeMapGetDelta,
warnPrematureAccess,
MapDelta, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
} from '../internals.js'
import * as iterator from 'lib0/iterator'
/**
* @template T
* @extends YEvent<YMap<T>>
* Event that describes the changes on a YMap.
*/
export class YMapEvent extends YEvent {
/**
* @param {YMap<T>} ymap The YArray that changed.
* @param {Transaction} transaction
* @param {Set<any>} subs The keys that changed.
*/
constructor (ymap, transaction, subs) {
super(ymap, transaction)
this.keysChanged = subs
}
}
import * as delta from 'lib0/delta' // eslint-disable-line
/**
* @template MapType
* A shared Map implementation.
*
* @extends AbstractType<YMapEvent<MapType>>
* @extends AbstractType<delta.MapDelta<{[K in string]:MapType}>>
* @implements {Iterable<[string, MapType]>}
*/
export class YMap extends AbstractType {
@@ -72,7 +55,7 @@ export class YMap extends AbstractType {
* * Observer functions are fired
*
* @param {Doc} y The Yjs instance
* @param {Item} item
* @param {Item?} item
*/
_integrate (y, item) {
super._integrate(y, item)
@@ -82,25 +65,15 @@ export class YMap extends AbstractType {
this._prelimContent = null
}
/**
* @return {YMap<MapType>}
*/
_copy () {
return new YMap()
}
/**
* 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 {YMap<MapType>}
* @return {this}
*/
clone () {
/**
* @type {YMap<MapType>}
*/
const map = new YMap()
const map = this._copy()
this.forEach((value, key) => {
map.set(key, value instanceof AbstractType ? /** @type {typeof value} */ (value.clone()) : value)
})
@@ -114,7 +87,7 @@ export class YMap extends AbstractType {
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*/
_callObserver (transaction, parentSubs) {
callTypeObservers(this, transaction, new YMapEvent(this, transaction, parentSubs))
callTypeObservers(this, transaction, new YEvent(this, transaction, parentSubs))
}
/**
@@ -187,22 +160,6 @@ export class YMap extends AbstractType {
})
}
/**
* 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 {import('../internals.js').AbstractAttributionManager} am
* @return {MapDelta<{[key:string]: MapType},undefined>} The Delta representation of this type.
*
* @public
*/
getContent (am) {
return typeMapGetDelta(this, am)
}
/**
* Returns an Iterator of [key, value] pairs
*
@@ -291,6 +248,7 @@ export class YMap extends AbstractType {
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
* @return {import('../utils/types.js').YType}
*
* @private
* @function

View File

@@ -25,27 +25,15 @@ import {
ContentType,
warnPrematureAccess,
noAttributionsManager, AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item, Transaction, // eslint-disable-line
createAttributionFromAttributionItems,
mergeIdSets,
diffIdSet,
createIdSet,
ContentDeleted
equalAttrs
} from '../internals.js'
import * as delta from '../utils/Delta.js'
import * as math from 'lib0/math'
import * as traits from 'lib0/traits'
import * as object from 'lib0/object'
import * as map from 'lib0/map'
import * as error from 'lib0/error'
/**
* @param {any} a
* @param {any} b
* @return {boolean}
*/
const equalAttrs = (a, b) => a === b || (typeof a === 'object' && typeof b === 'object' && a && b && object.equalFlat(a, b))
import * as delta from 'lib0/delta'
export class ItemTextListPosition {
/**
@@ -86,7 +74,7 @@ export class ItemTextListPosition {
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {import('../utils/types.js').YType} parent
* @param {number} length
* @param {Object<string,any>} attributes
*
@@ -214,7 +202,7 @@ const findNextPosition = (transaction, pos, count) => {
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {import('../utils/types.js').YType} parent
* @param {number} index
* @param {boolean} useSearchMarker
* @return {ItemTextListPosition}
@@ -238,7 +226,7 @@ const findPosition = (transaction, parent, index, useSearchMarker) => {
* Negate applied formats
*
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {import('../utils/types.js').YType} parent
* @param {ItemTextListPosition} currPos
* @param {Map<string,any>} negatedAttributes
*
@@ -311,7 +299,7 @@ const minimizeAttributeChanges = (currPos, attributes) => {
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {import('../utils/types.js').YType} parent
* @param {ItemTextListPosition} currPos
* @param {Object<string,any>} attributes
* @return {Map<string,any>}
@@ -341,9 +329,9 @@ const insertAttributes = (transaction, parent, currPos, attributes) => {
/**
* @param {Transaction} transaction
* @param {AbstractType<any>} parent
* @param {import('../utils/types.js').YType} parent
* @param {ItemTextListPosition} currPos
* @param {string|object|AbstractType<any>} text
* @param {string|object|import('../utils/types.js').YType} text
* @param {Object<string,any>} attributes
*
* @private
@@ -638,82 +626,6 @@ const deleteText = (transaction, currPos, length) => {
* @typedef {Object} TextAttributes
*/
/**
* @template {{ [key:string]: any } | AbstractType<any> } TextEmbeds
* @extends YEvent<YText<any>>
* Event that describes the changes on a YText type.
*/
export class YTextEvent extends YEvent {
/**
* @param {YText<TextEmbeds>} ytext
* @param {Transaction} transaction
* @param {Set<any>} subs The keys that changed
*/
constructor (ytext, transaction, subs) {
super(ytext, transaction)
/**
* Whether the children changed.
* @type {Boolean}
* @private
*/
this.childListChanged = false
/**
* Set of all changed attributes.
* @type {Set<string>}
*/
this.keysChanged = new Set()
subs.forEach((sub) => {
if (sub === null) {
this.childListChanged = true
} else {
this.keysChanged.add(sub)
}
})
}
/**
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:delta.TextDelta<TextEmbeds,undefined>}}
*/
get changes () {
if (this._changes === null) {
/**
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:delta.TextDelta<TextEmbeds,undefined>}}
*/
const changes = {
keys: this.keys,
delta: this.delta,
added: new Set(),
deleted: new Set()
}
this._changes = changes
}
return /** @type {any} */ (this._changes)
}
/**
* @param {AbstractAttributionManager} am
* @return {import('../utils/Delta.js').TextDelta<TextEmbeds,undefined>} The Delta representation of this type.
*
* @public
*/
getDelta (am = noAttributionsManager) {
const itemsToRender = mergeIdSets([diffIdSet(this.transaction.insertSet, this.transaction.deleteSet), diffIdSet(this.transaction.deleteSet, this.transaction.insertSet)])
return this.target.getContent(am, { itemsToRender, retainDeletes: true })
}
/**
* 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.TextDelta<TextEmbeds,undefined>}
*
* @public
*/
get delta () {
return this._delta ?? (this._delta = this.getDelta())
}
}
/**
* Type that represents text with formatting information.
*
@@ -721,8 +633,8 @@ export class YTextEvent extends YEvent {
* block formats (format information on a paragraph), embeds (complex elements
* like pictures and videos), and text formats (**bold**, *italic*).
*
* @template {{ [key:string]:any } | AbstractType<any>} [Embeds={ [key:string]:any } | AbstractType<any>]
* @extends AbstractType<YTextEvent<Embeds>>
* @template {{ [key:string]:any } | import('../utils/types.js').YType} [Embeds={ [key:string]:any } | import('../utils/types.js').YType]
* @extends {AbstractType<delta.TextDelta<Embeds>>}
*/
export class YText extends AbstractType {
/**
@@ -758,7 +670,7 @@ export class YText extends AbstractType {
/**
* @param {Doc} y
* @param {Item} item
* @param {Item?} item
*/
_integrate (y, item) {
super._integrate(y, item)
@@ -770,13 +682,6 @@ export class YText extends AbstractType {
this._pending = null
}
/**
* @return {YText<Embeds>}
*/
_copy () {
return new YText()
}
/**
* Makes a copy of this data type that can be included somewhere else.
*
@@ -788,7 +693,7 @@ export class YText extends AbstractType {
/**
* @type {YText<Embeds>}
*/
const text = new YText()
const text = /** @type {any} */ (new YText())
text.applyDelta(this.getContent())
return text
}
@@ -801,8 +706,8 @@ export class YText extends AbstractType {
*/
_callObserver (transaction, parentSubs) {
super._callObserver(transaction, parentSubs)
const event = new YTextEvent(this, transaction, parentSubs)
callTypeObservers(this, transaction, event)
const event = new YEvent(/** @type {YText<any>} */ (this), transaction, parentSubs)
callTypeObservers(/** @type {YText<any>} */ (this), transaction, event)
// If a remote change happened, we try to cleanup potential formatting duplicates.
if (!transaction.local && this._hasFormatting) {
transaction._needFormattingCleanup = true
@@ -843,272 +748,32 @@ export class YText extends AbstractType {
/**
* Apply a {@link Delta} on this shared YText type.
*
* @param {Array<any> | delta.TextDelta<Embeds,undefined>} delta The changes to apply on this element.
* @param {delta.TextDelta<Embeds>} d The changes to apply on this element.
* @param {AbstractAttributionManager} am
*
* @public
*/
applyDelta (delta, am = noAttributionsManager) {
applyDelta (d, am = noAttributionsManager) {
if (this.doc !== null) {
transact(this.doc, transaction => {
const deltaOps = /** @type {Array<any>} */ (/** @type {delta.TextDelta<any,undefined>} */ (delta).ops instanceof Array ? /** @type {delta.TextDelta<any,undefined>} */ (delta).ops : delta)
const currPos = new ItemTextListPosition(null, this._start, 0, new Map(), am)
for (let i = 0; i < deltaOps.length; i++) {
const op = deltaOps[i]
if (op.insert !== undefined) {
for (const op of d.children) {
if (delta.$insertOp.check(op)) {
if (op.insert.length > 0 || typeof op.insert !== 'string') {
insertText(transaction, this, currPos, op.insert, op.attributes || {})
insertText(transaction, this, currPos, op.insert, op.format || {})
}
} else if (op.retain !== undefined) {
currPos.formatText(transaction, this, op.retain, op.attributes || {})
} else if (op.delete !== undefined) {
} else if (delta.$retainOp.check(op)) {
currPos.formatText(transaction, this, op.retain, op.format || {})
} else if (delta.$deleteOp.check(op)) {
deleteText(transaction, currPos, op.delete)
}
}
})
} else {
/** @type {Array<function>} */ (this._pending).push(() => this.applyDelta(delta))
/** @type {Array<function>} */ (this._pending).push(() => this.applyDelta(d))
}
}
/**
* 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 {import('../utils/Delta.js').TextDelta<Embeds extends import('./AbstractType.js').AbstractType<infer SubEvent> ? SubEvent : Embeds, undefined>} The Delta representation of this type.
*
* @public
*/
getContentDeep (am = noAttributionsManager) {
return this.getContent(am).map(d =>
d instanceof delta.InsertEmbedOp && d.insert instanceof AbstractType
? new delta.InsertEmbedOp(d.insert.getContent(am), d.attributes, d.attribution)
: d
)
}
/**
* 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
* @param {Object} [opts]
* @param {import('../utils/IdSet.js').IdSet?} [opts.itemsToRender]
* @param {boolean} [opts.retainInserts] - if true, retain rendered inserts with attributions
* @param {boolean} [opts.retainDeletes] - if true, retain rendered+attributed deletes only
* @return {import('../utils/Delta.js').TextDelta<Embeds,undefined>} The Delta representation of this type.
*
* @public
*/
getContent (am = noAttributionsManager, { itemsToRender = null, retainInserts = false, retainDeletes = false } = {}) {
/**
* @type {import('../utils/Delta.js').TextDeltaBuilder<Embeds>}
*/
const d = delta.createTextDelta()
/**
* @type {import('../utils/Delta.js').FormattingAttributes}
*/
let currentAttributes = {} // saves all current attributes for insert
let usingCurrentAttributes = false
/**
* @type {import('../utils/Delta.js').FormattingAttributes}
*/
let changedAttributes = {} // saves changed attributes for retain
let usingChangedAttributes = false
/**
* Logic for formatting attribute attribution
* Everything that comes after an formatting attribute is formatted by the user that created it.
* Two exceptions:
* - the user resets formatting to the previously known formatting that is not attributed
* - the user deletes a formatting attribute and hence restores the previously known formatting
* that is not attributed.
* @type {import('../utils/Delta.js').FormattingAttributes}
*/
const previousUnattributedAttributes = {} // contains previously known unattributed formatting
/**
* @type {import('../utils/Delta.js').FormattingAttributes}
*/
const previousAttributes = {} // The value before changes
/**
* @type {Array<import('../internals.js').AttributedContent<any>>}
*/
const cs = []
for (let item = this._start; item !== null; cs.length = 0) {
if (itemsToRender != null) {
for (; item !== null && cs.length < 50; item = item.right) {
const rslice = itemsToRender.slice(item.id.client, item.id.clock, item.length)
let itemContent = rslice.length > 1 ? item.content.copy() : item.content
for (let ir = 0; ir < rslice.length; ir++) {
const idrange = rslice[ir]
const content = itemContent
if (ir !== rslice.length - 1) {
itemContent = itemContent.splice(idrange.len)
}
am.readContent(cs, item.id.client, idrange.clock, item.deleted, content, idrange.exists ? 2 : 0)
}
}
} else {
for (; item !== null && cs.length < 50; item = item.right) {
am.readContent(cs, item.id.client, item.id.clock, item.deleted, item.content, 1)
}
}
for (let i = 0; i < cs.length; i++) {
const c = cs[i]
// render (attributed) content even if it was deleted
const renderContent = c.render && (!c.deleted || c.attrs != null)
// content that was just deleted. It is not rendered as an insertion, because it doesn't
// have any attributes.
const renderDelete = c.render && c.deleted
// existing content that should be retained, only adding changed attributes
const retainContent = !c.render && (!c.deleted || c.attrs != null)
const attribution = (renderContent || c.content.constructor === ContentFormat) ? createAttributionFromAttributionItems(c.attrs, c.deleted) : null
switch (c.content.constructor) {
case ContentDeleted: {
if (renderDelete) d.delete(c.content.getLength())
break
}
case ContentType:
case ContentEmbed:
if (renderContent) {
d.usedAttributes = currentAttributes
usingCurrentAttributes = true
if (c.deleted ? retainDeletes : retainInserts) {
d.retain(c.content.getLength(), null, attribution ?? {})
} else {
d.insert(c.content.getContent()[0], null, attribution)
}
} else if (renderDelete) {
d.delete(1)
} else if (retainContent) {
d.usedAttributes = changedAttributes
usingChangedAttributes = true
d.retain(1)
}
break
case ContentString:
if (renderContent) {
d.usedAttributes = currentAttributes
usingCurrentAttributes = true
if (c.deleted ? retainDeletes : retainInserts) {
d.retain(/** @type {ContentString} */ (c.content).str.length, null, attribution ?? {})
} else {
d.insert(/** @type {ContentString} */ (c.content).str, null, attribution)
}
} else if (renderDelete) {
d.delete(c.content.getLength())
} else if (retainContent) {
d.usedAttributes = changedAttributes
usingChangedAttributes = true
d.retain(c.content.getLength())
}
break
case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (c.content)
const currAttrVal = currentAttributes[key] ?? null
if (attribution != null && (c.deleted || !object.hasProperty(previousUnattributedAttributes, key))) {
previousUnattributedAttributes[key] = c.deleted ? value : currAttrVal
}
// @todo write a function "updateCurrentAttributes" and "updateChangedAttributes"
// # Update Attributes
if (renderContent || renderDelete) {
// create fresh references
if (usingCurrentAttributes) {
currentAttributes = object.assign({}, currentAttributes)
usingCurrentAttributes = false
}
if (usingChangedAttributes) {
usingChangedAttributes = false
changedAttributes = object.assign({}, changedAttributes)
}
}
if (renderContent || renderDelete) {
if (c.deleted) {
// content was deleted, but is possibly attributed
if (!equalAttrs(value, currAttrVal)) { // do nothing if nothing changed
if (equalAttrs(currAttrVal, previousAttributes[key] ?? null) && changedAttributes[key] !== undefined) {
delete changedAttributes[key]
} else {
changedAttributes[key] = currAttrVal
}
// current attributes doesn't change
previousAttributes[key] = value
}
} else { // !c.deleted
// content was inserted, and is possibly attributed
if (equalAttrs(value, currAttrVal)) {
// item.delete(transaction)
} else if (equalAttrs(value, previousAttributes[key] ?? null)) {
delete changedAttributes[key]
} else {
changedAttributes[key] = value
}
if (value == null) {
delete currentAttributes[key]
} else {
currentAttributes[key] = value
}
}
} else if (retainContent && !c.deleted) {
// fresh reference to currentAttributes only
if (usingCurrentAttributes) {
currentAttributes = object.assign({}, currentAttributes)
usingCurrentAttributes = false
}
if (usingChangedAttributes && changedAttributes[key] !== undefined) {
usingChangedAttributes = false
changedAttributes = object.assign({}, changedAttributes)
}
if (value == null) {
delete currentAttributes[key]
} else {
currentAttributes[key] = value
}
delete changedAttributes[key]
previousAttributes[key] = value
}
// # Update Attributions
if (attribution != null || object.hasProperty(previousUnattributedAttributes, key)) {
/**
* @type {import('../utils/AttributionManager.js').Attribution}
*/
const formattingAttribution = object.assign({}, d.usedAttribution)
const changedAttributedAttributes = /** @type {{ [key: string]: Array<any> }} */ (formattingAttribution.attributes = object.assign({}, formattingAttribution.attributes ?? {}))
if (attribution == null || equalAttrs(previousUnattributedAttributes[key], currentAttributes[key] ?? null)) {
// an unattributed formatting attribute was found or an attributed formatting
// attribute was found that resets to the previous status
delete changedAttributedAttributes[key]
delete previousUnattributedAttributes[key]
} else {
const by = changedAttributedAttributes[key] = (changedAttributedAttributes[key]?.slice() ?? [])
by.push(...((c.deleted ? attribution.delete : attribution.insert) ?? []))
const attributedAt = (c.deleted ? attribution.deletedAt : attribution.insertedAt)
if (attributedAt) formattingAttribution.attributedAt = attributedAt
}
if (object.isEmpty(changedAttributedAttributes)) {
d.useAttribution(null)
} else if (attribution != null) {
const attributedAt = (c.deleted ? attribution.deletedAt : attribution.insertedAt)
if (attributedAt != null) formattingAttribution.attributedAt = attributedAt
d.useAttribution(formattingAttribution)
}
}
break
}
}
}
}
// @todo! fix the typings here
return /** @type {any} */ (d.done())
}
/**
* Insert text at a given index.
*
@@ -1295,7 +960,7 @@ export class YText extends AbstractType {
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
* @return {YText}
* @return {import('../utils/types.js').YType}
*
* @private
* @function

View File

@@ -9,15 +9,10 @@ import {
typeMapGet,
typeMapGetAll,
typeMapGetAllSnapshot,
typeListForEach,
YXmlElementRefID,
typeMapGetDelta,
noAttributionsManager,
AbstractAttributionManager, Snapshot, YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item, // eslint-disable-line
Snapshot, YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item, // eslint-disable-line
} from '../internals.js'
import * as delta from '../utils/Delta.js'
/**
* @typedef {Object|number|null|Array<any>|string|Uint8Array|AbstractType<any>} ValueTypes
*/
@@ -29,7 +24,9 @@ import * as delta from '../utils/Delta.js'
* * An YXmlElement has attributes (key value pairs)
* * An YXmlElement has childElements that must inherit from YXmlElement
*
* @template {{ [key: string]: ValueTypes }} [KV={ [key: string]: string }]
* @template {{ [key: string]: ValueTypes }} [Attrs={ [key: string]: string }]
* @template {any} [Children=any]
* @extends YXmlFragment<Children,Attrs>
*/
export class YXmlElement extends YXmlFragment {
constructor (nodeName = 'UNDEFINED') {
@@ -65,7 +62,7 @@ export class YXmlElement extends YXmlFragment {
* * Observer functions are fired
*
* @param {Doc} y The Yjs instance
* @param {Item} item
* @param {Item?} item
*/
_integrate (y, item) {
super._integrate(y, item)
@@ -78,10 +75,10 @@ export class YXmlElement extends YXmlFragment {
/**
* Creates an Item with the same effect as this Item (without position effect)
*
* @return {YXmlElement}
* @return {this}
*/
_copy () {
return new YXmlElement(this.nodeName)
return /** @type {any} */ (new YXmlElement(this.nodeName))
}
/**
@@ -89,13 +86,10 @@ export class YXmlElement extends YXmlFragment {
*
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
*
* @return {YXmlElement<KV>}
* @return {this}
*/
clone () {
/**
* @type {YXmlElement<KV>}
*/
const el = new YXmlElement(this.nodeName)
const el = this._copy()
const attrs = this.getAttributes()
object.forEach(attrs, (value, key) => {
if (typeof value === 'string') {
@@ -154,17 +148,17 @@ export class YXmlElement extends YXmlFragment {
/**
* Sets or updates an attribute.
*
* @template {keyof KV & string} KEY
* @template {keyof Attrs & string} KEY
*
* @param {KEY} attributeName The attribute name that is to be set.
* @param {KV[KEY]} attributeValue The attribute value 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, attributeValue)
typeMapSet(transaction, this, attributeName, /** @type {any} */ (attributeValue))
})
} else {
/** @type {Map<string, any>} */ (this._prelimAttrs).set(attributeName, attributeValue)
@@ -174,11 +168,11 @@ export class YXmlElement extends YXmlFragment {
/**
* Returns an attribute value that belongs to the attribute name.
*
* @template {keyof KV & string} KEY
* @template {keyof Attrs & string} KEY
*
* @param {KEY} attributeName The attribute name that identifies the
* queried value.
* @return {KV[KEY]|undefined} The queried attribute value.
* @return {Attrs[KEY]|undefined} The queried attribute value.
*
* @public
*/
@@ -202,7 +196,7 @@ export class YXmlElement extends YXmlFragment {
* Returns all attribute name/value pairs in a JSON Object.
*
* @param {Snapshot} [snapshot]
* @return {{ [Key in Extract<keyof KV,string>]?: KV[Key]}} A JSON Object that describes the attributes.
* @return {{ [Key in Extract<keyof Attrs,string>]?: Attrs[Key]}} A JSON Object that describes the attributes.
*
* @public
*/
@@ -210,93 +204,6 @@ export class YXmlElement extends YXmlFragment {
return /** @type {any} */ (snapshot ? typeMapGetAllSnapshot(this, snapshot) : typeMapGetAll(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 {{ nodeName: string, children: delta.ArrayDeltaBuilder<Array<import('./AbstractType.js').DeepContent>>, attributes: import('./AbstractType.js').MapAttributedContent<any> }}
*
* @public
*/
getContentDeep (am = noAttributionsManager) {
const { children: origChildren, attributes: origAttributes } = this.getContent(am)
const children = origChildren.map(d => /** @type {any} */ (
(d instanceof delta.InsertArrayOp && d.insert instanceof Array)
? new delta.InsertArrayOp(d.insert.map(e => e instanceof AbstractType ? /** @type {delta.ArrayDeltaBuilder<Array<any>>} */ (e.getContentDeep(am)) : e), d.attributes, d.attribution)
: d
))
/**
* @todo there is a Attributes type and a DeepAttributes type.
* @type {delta.MapDeltaBuilder<any,any>}
*/
const attributes = delta.createMapDelta()
origAttributes.forEach(
null,
(insertOp, key) => {
if (insertOp.value instanceof AbstractType) {
attributes.set(key, insertOp.value.getContentDeep(am), null, insertOp.attribution)
} else {
attributes.set(key, insertOp.value, undefined, insertOp.attribution)
}
}
)
return delta.createXmlDelta(this.nodeName, children, attributes)
}
/**
* 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 {import('../internals.js').AbstractAttributionManager} am
*
* @public
*/
getContent (am = noAttributionsManager) {
const { children } = super.getContent(am)
const attributes = typeMapGetDelta(this, am)
return new delta.XmlDelta(this.nodeName, children, attributes)
}
/**
* Creates a Dom Element that mirrors this YXmlElement.
*
* @param {Document} [_document=document] The document object (you must define
* this when calling this method in
* nodejs)
* @param {Object<string, any>} [hooks={}] Optional property to customize how hooks
* are presented in the DOM
* @param {any} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type.
* @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
*
* @public
*/
toDOM (_document = document, hooks = {}, binding) {
const dom = _document.createElement(this.nodeName)
const attrs = this.getAttributes()
for (const key in attrs) {
const value = attrs[key]
if (typeof value === 'string') {
dom.setAttribute(key, value)
}
}
typeListForEach(this, yxml => {
dom.appendChild(yxml.toDOM(_document, hooks, binding))
})
if (binding !== undefined) {
binding._createAssociation(dom, this)
}
return dom
}
/**
* Transform the properties of this type to binary and write it to an
* BinaryEncoder.
@@ -313,7 +220,7 @@ export class YXmlElement extends YXmlFragment {
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {YXmlElement}
* @return {import('../utils/types.js').YType}
*
* @function
*/

View File

@@ -1,39 +0,0 @@
import {
YEvent,
YXmlText, YXmlElement, YXmlFragment, Transaction // eslint-disable-line
} from '../internals.js'
/**
* @extends YEvent<YXmlElement|YXmlText|YXmlFragment>
* An Event that describes changes on a YXml Element or Yxml Fragment
*/
export class YXmlEvent extends YEvent {
/**
* @param {YXmlElement|YXmlText|YXmlFragment} target The target on which the event is created.
* @param {Set<string|null>} subs The set of changed attributes. `null` is included if the
* child list changed.
* @param {Transaction} transaction The transaction instance with which the
* change was created.
*/
constructor (target, subs, transaction) {
super(target, transaction)
/**
* Whether the children changed.
* @type {Boolean}
* @private
*/
this.childListChanged = false
/**
* Set of all changed attributes.
* @type {Set<string>}
*/
this.attributesChanged = new Set()
subs.forEach((sub) => {
if (sub === null) {
this.childListChanged = true
} else {
this.attributesChanged.add(sub)
}
})
}
}

View File

@@ -3,8 +3,7 @@
*/
import {
YXmlEvent,
YXmlElement,
YEvent,
AbstractType,
typeListMap,
typeListForEach,
@@ -18,14 +17,11 @@ import {
typeListGet,
typeListSlice,
warnPrematureAccess,
noAttributionsManager,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, // eslint-disable-line
typeListGetContent
YXmlElement, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, YXmlText, YXmlHook // eslint-disable-line
} from '../internals.js'
import * as delta from '../utils/Delta.js'
import * as delta from 'lib0/delta' // eslint-disable-line
import * as error from 'lib0/error'
import * as array from 'lib0/array'
/**
* Define the elements to which a set of CSS queries apply.
@@ -48,83 +44,6 @@ import * as array from 'lib0/array'
* @return {boolean} Whether to include the Dom node in the YXmlElement.
*/
/**
* Represents a subset of the nodes of a YXmlElement / YXmlFragment and a
* position within them.
*
* Can be created with {@link YXmlFragment#createTreeWalker}
*
* @public
* @implements {Iterable<YXmlElement|YXmlText|YXmlElement|YXmlHook>}
*/
export class YXmlTreeWalker {
/**
* @param {YXmlFragment | YXmlElement} root
* @param {function(AbstractType<any>):boolean} [f]
*/
constructor (root, f = () => true) {
this._filter = f
this._root = root
/**
* @type {Item}
*/
this._currentNode = /** @type {Item} */ (root._start)
this._firstCall = true
root.doc ?? warnPrematureAccess()
}
[Symbol.iterator] () {
return this
}
/**
* Get the next node.
*
* @return {IteratorResult<YXmlElement|YXmlText|YXmlHook>} The next node.
*
* @public
*/
next () {
/**
* @type {Item|null}
*/
let n = this._currentNode
let type = n && n.content && /** @type {any} */ (n.content).type
if (n !== null && (!this._firstCall || n.deleted || !this._filter(type))) { // if first call, we check if we can use the first item
do {
type = /** @type {any} */ (n.content).type
if (!n.deleted && (type.constructor === YXmlElement || type.constructor === YXmlFragment) && type._start !== null) {
// walk down in the tree
n = type._start
} else {
// walk right or up in the tree
while (n !== null) {
/**
* @type {Item | null}
*/
const nxt = n.next
if (nxt !== null) {
n = nxt
break
} else if (n.parent === this._root) {
n = null
} else {
n = /** @type {AbstractType<any>} */ (n.parent)._item
}
}
}
} while (n !== null && (n.deleted || !this._filter(/** @type {ContentType} */ (n.content).type)))
}
this._firstCall = false
if (n === null) {
// @ts-ignore
return { value: undefined, done: true }
}
this._currentNode = n
return { value: /** @type {any} */ (n.content).type, done: false }
}
}
/**
* Represents a list of {@link YXmlElement}.and {@link YXmlText} types.
* A YxmlFragment is similar to a {@link YXmlElement}, but it does not have a
@@ -132,7 +51,9 @@ export class YXmlTreeWalker {
* element - in this case the attributes and the nodeName are not shared.
*
* @public
* @extends AbstractType<YXmlEvent>
* @template {any} [Children=any]
* @template {{[K in string]:any}} [Attrs={}]
* @extends AbstractType<delta.Delta<any,Attrs,Children,any>>
*/
export class YXmlFragment extends AbstractType {
constructor () {
@@ -159,7 +80,7 @@ export class YXmlFragment extends AbstractType {
* * Observer functions are fired
*
* @param {Doc} y The Yjs instance
* @param {Item} item
* @param {Item?} item
*/
_integrate (y, item) {
super._integrate(y, item)
@@ -167,20 +88,15 @@ export class YXmlFragment extends AbstractType {
this._prelimContent = null
}
_copy () {
return new YXmlFragment()
}
/**
* 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 {YXmlFragment}
* @return {this}
*/
clone () {
const el = new YXmlFragment()
// @ts-ignore
const el = this._copy()
el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item))
return el
}
@@ -190,71 +106,6 @@ export class YXmlFragment extends AbstractType {
return this._prelimContent === null ? this._length : this._prelimContent.length
}
/**
* Create a subtree of childNodes.
*
* @example
* const walker = elem.createTreeWalker(dom => dom.nodeName === 'div')
* for (let node in walker) {
* // `node` is a div node
* nop(node)
* }
*
* @param {function(AbstractType<any>):boolean} filter Function that is called on each child element and
* returns a Boolean indicating whether the child
* is to be included in the subtree.
* @return {YXmlTreeWalker} A subtree and a position within it.
*
* @public
*/
createTreeWalker (filter) {
return new YXmlTreeWalker(this, filter)
}
/**
* Returns the first YXmlElement that matches the query.
* Similar to DOM's {@link querySelector}.
*
* Query support:
* - tagname
* TODO:
* - id
* - attribute
*
* @param {CSS_Selector} query The query on the children.
* @return {YXmlElement|YXmlText|YXmlHook|null} The first element that matches the query or null.
*
* @public
*/
querySelector (query) {
query = query.toUpperCase()
// @ts-ignore
const iterator = new YXmlTreeWalker(this, element => element.nodeName && element.nodeName.toUpperCase() === query)
const next = iterator.next()
if (next.done) {
return null
} else {
return next.value
}
}
/**
* Returns all YXmlElements that match the query.
* Similar to Dom's {@link querySelectorAll}.
*
* @todo Does not yet support all queries. Currently only query by tagName.
*
* @param {CSS_Selector} query The query on the children
* @return {Array<YXmlElement|YXmlText|YXmlHook|null>} The elements that match this query.
*
* @public
*/
querySelectorAll (query) {
query = query.toUpperCase()
// @ts-ignore
return array.from(new YXmlTreeWalker(this, element => element.nodeName && element.nodeName.toUpperCase() === query))
}
/**
* Creates YXmlEvent and calls observers.
*
@@ -262,7 +113,7 @@ export class YXmlFragment extends AbstractType {
* @param {Set<null|string>} parentSubs Keys changed on this type. `null` if list was modified.
*/
_callObserver (transaction, parentSubs) {
callTypeObservers(this, transaction, new YXmlEvent(this, parentSubs, transaction))
callTypeObservers(this, transaction, new YEvent(this, transaction, parentSubs))
}
/**
@@ -281,32 +132,6 @@ export class YXmlFragment extends AbstractType {
return this.toString()
}
/**
* Creates a Dom Element that mirrors this YXmlElement.
*
* @param {Document} [_document=document] The document object (you must define
* this when calling this method in
* nodejs)
* @param {Object<string, any>} [hooks={}] Optional property to customize how hooks
* are presented in the DOM
* @param {any} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type.
* @return {Node} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
*
* @public
*/
toDOM (_document = document, hooks = {}, binding) {
const fragment = _document.createDocumentFragment()
if (binding !== undefined) {
binding._createAssociation(fragment, this)
}
typeListForEach(this, xmlType => {
fragment.insertBefore(xmlType.toDOM(_document, hooks, binding), null)
})
return fragment
}
/**
* Inserts new content at an index.
*
@@ -315,7 +140,7 @@ export class YXmlFragment extends AbstractType {
* xml.insert(0, [new Y.XmlText('text')])
*
* @param {number} index The index to insert content at
* @param {Array<YXmlElement|YXmlText>} content The array of content
* @param {Array<YXmlElement|YXmlText|YXmlHook>} content The array of content
*/
insert (index, content) {
if (this.doc !== null) {
@@ -380,36 +205,6 @@ export class YXmlFragment extends AbstractType {
return typeListToArray(this)
}
/**
* Calculate the attributed content using the attribution manager.
*
* @param {import('../internals.js').AbstractAttributionManager} am
* @return {{ children: import('../utils/Delta.js').ArrayDeltaBuilderBuilder<Array<YXmlElement|YXmlText|YXmlHook>> }}
*/
getContent (am = noAttributionsManager) {
const children = typeListGetContent(this, am)
return { children }
}
/**
* Calculate the attributed content using the attribution manager.
*
* @param {import('../internals.js').AbstractAttributionManager} am
* @return {{ children: import('../utils/Delta.js').ArrayDeltaBuilderBuilder<Array<import('./AbstractType.js').YXmlDeepContent>> }}
*/
getContentDeep (am) {
const { children: origChildren } = this.getContent(am)
/**
* @type {import('../utils/Delta.js').ArrayDeltaBuilderBuilder<Array<import('./AbstractType.js').YXmlDeepContent>>}
*/
const children = origChildren.map(d => /** @type {any} */ (
d instanceof delta.InsertArrayOp && d.insert instanceof Array
? new delta.InsertArrayOp(d.insert.map(e => e instanceof AbstractType ? e.getContentDeep(am) : e), d.attributes, d.attribution)
: d
))
return { children }
}
/**
* Appends content to this YArray.
*
@@ -474,7 +269,7 @@ export class YXmlFragment extends AbstractType {
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
* @return {YXmlFragment}
* @return {import('../utils/types.js').YType}
*
* @private
* @function

View File

@@ -22,10 +22,10 @@ export class YXmlHook extends YMap {
}
/**
* Creates an Item with the same effect as this Item (without position effect)
* @return {this}
*/
_copy () {
return new YXmlHook(this.hookName)
return /** @type {this} */ (new YXmlHook(this.hookName))
}
/**
@@ -33,46 +33,16 @@ export class YXmlHook extends YMap {
*
* Note that the content is only readable _after_ it has been included somewhere in the Ydoc.
*
* @return {YXmlHook}
* @return {this}
*/
clone () {
const el = new YXmlHook(this.hookName)
const el = this._copy()
this.forEach((value, key) => {
el.set(key, value)
})
return el
}
/**
* Creates a Dom Element that mirrors this YXmlElement.
*
* @param {Document} [_document=document] The document object (you must define
* this when calling this method in
* nodejs)
* @param {Object.<string, any>} [hooks] Optional property to customize how hooks
* are presented in the DOM
* @param {any} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type
* @return {Element} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
*
* @public
*/
toDOM (_document = document, hooks = {}, binding) {
const hook = hooks[this.hookName]
let dom
if (hook !== undefined) {
dom = hook.createDom(this)
} else {
dom = document.createElement(this.hookName)
}
dom.setAttribute('data-yjs-hook', this.hookName)
if (binding !== undefined) {
binding._createAssociation(dom, this)
}
return dom
}
/**
* Transform the properties of this type to binary and write it to an
* BinaryEncoder.
@@ -89,7 +59,7 @@ export class YXmlHook extends YMap {
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {YXmlHook}
* @return {import('../utils/types.js').YType}
*
* @private
* @function

View File

@@ -4,11 +4,12 @@ import {
ContentType, YXmlElement, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, // eslint-disable-line
} from '../internals.js'
import * as delta from '../utils/Delta.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 {
/**
@@ -27,82 +28,19 @@ export class YXmlText extends YText {
return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null
}
_copy () {
return new YXmlText()
}
/**
* 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 {YXmlText}
* @return {this}
*/
clone () {
const text = new YXmlText()
const text = /** @type {this} */ (this._copy())
text.applyDelta(this.getContent())
return text
}
/**
* Creates a Dom Element that mirrors this YXmlText.
*
* @param {Document} [_document=document] The document object (you must define
* this when calling this method in
* nodejs)
* @param {Object<string, any>} [hooks] Optional property to customize how hooks
* are presented in the DOM
* @param {any} [binding] You should not set this property. This is
* used if DomBinding wants to create a
* association to the created DOM type.
* @return {Text} The {@link https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element}
*
* @public
*/
toDOM (_document = document, hooks, binding) {
const dom = _document.createTextNode(this.toString())
if (binding !== undefined) {
binding._createAssociation(dom, this)
}
return dom
}
toString () {
return this.getContent().ops.map(dop => {
if (dop instanceof delta.InsertStringOp) {
const nestedNodes = []
for (const nodeName in dop.attributes) {
const attrs = []
for (const key in dop.attributes[nodeName]) {
attrs.push({ key, value: dop.attributes[nodeName][key] })
}
// sort attributes to get a unique order
attrs.sort((a, b) => a.key < b.key ? -1 : 1)
nestedNodes.push({ nodeName, attrs })
}
// sort node order to get a unique order
nestedNodes.sort((a, b) => a.nodeName < b.nodeName ? -1 : 1)
// now convert to dom string
let str = ''
for (let i = 0; i < nestedNodes.length; i++) {
const node = nestedNodes[i]
str += `<${node.nodeName}`
for (let j = 0; j < node.attrs.length; j++) {
const attr = node.attrs[j]
str += ` ${attr.key}="${attr.value}"`
}
str += '>'
}
str += dop.insert
for (let i = nestedNodes.length - 1; i >= 0; i--) {
str += `</${nestedNodes[i].nodeName}>`
}
return str
}
return ''
}).join('')
}
/**
* @return {string}
*/
@@ -119,10 +57,10 @@ export class YXmlText extends YText {
}
/**
* @param {UpdateDecoderV1 | UpdateDecoderV2} decoder
* @return {YXmlText}
* @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder
* @return {import('../utils/types.js').YType}
*
* @private
* @function
*/
export const readYXmlText = decoder => new YXmlText()
export const readYXmlText = _decoder => new YXmlText()

View File

@@ -33,13 +33,13 @@ import { ObservableV2 } from 'lib0/observable'
import * as encoding from 'lib0/encoding'
import * as s from 'lib0/schema'
export const attributionJsonSchema = s.object({
insert: s.array(s.string).optional,
insertedAt: s.number.optional,
delete: s.array(s.string).optional,
deletedAt: s.number.optional,
attributes: s.record(s.string, s.array(s.string)).optional,
attributedAt: s.number.optional
export const attributionJsonSchema = s.$object({
insert: s.$array(s.$string).optional,
insertedAt: s.$number.optional,
delete: s.$array(s.$string).optional,
deletedAt: s.$number.optional,
attributes: s.$record(s.$string, s.$array(s.$string)).optional,
attributedAt: s.$number.optional
})
/**
@@ -63,7 +63,7 @@ export const createAttributionFromAttributionItems = (attrs, deleted) => {
*/
const attribution = {}
if (deleted) {
attribution.delete = s.array(s.string).ensure([])
attribution.delete = s.$array(s.$string).cast([])
} else {
attribution.insert = []
}
@@ -72,7 +72,7 @@ export const createAttributionFromAttributionItems = (attrs, deleted) => {
// eslint-disable-next-line no-fallthrough
case 'insert':
case 'delete': {
const as = /** @type {import('../utils/Delta.js').Attribution_} */ (attribution)
const as = /** @type {import('lib0/delta').Attribution} */ (attribution)
const ls = as[attr.name] = as[attr.name] ?? []
ls.push(attr.val)
break

File diff suppressed because it is too large Load Diff

View File

@@ -12,7 +12,7 @@ import {
YXmlFragment,
transact,
applyUpdate,
ContentDoc, Item, Transaction, YEvent, // eslint-disable-line
ContentDoc, Item, Transaction, // eslint-disable-line
encodeStateAsUpdate
} from '../internals.js'
@@ -24,6 +24,13 @@ import * as promise from 'lib0/promise'
export const generateNewClientId = random.uint32
/**
* @typedef {import('../utils/types.js').YTypeConstructors} YTypeConstructors
*/
/**
* @typedef {import('../utils/types.js').YType} YType
*/
/**
* @typedef {Object} DocOpts
* @property {boolean} [DocOpts.gc=true] Disable garbage collection (default: gc=true)
@@ -72,7 +79,7 @@ export class Doc extends ObservableV2 {
this.isSuggestionDoc = isSuggestionDoc
this.cleanupFormatting = !isSuggestionDoc
/**
* @type {Map<string, AbstractType<YEvent<any>>>}
* @type {Map<string, YType>}
*/
this.share = new Map()
this.store = new StructStore()
@@ -205,7 +212,7 @@ export class Doc extends ObservableV2 {
* 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)`, ..
*
* @template {typeof AbstractType<any>} Type
* @template {YTypeConstructors} TypeC
* @example
* const ydoc = new Y.Doc(..)
* const appState = {
@@ -214,8 +221,8 @@ export class Doc extends ObservableV2 {
* }
*
* @param {string} name
* @param {Type} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
* @return {InstanceType<Type>} The created type. Constructed with TypeConstructor
* @param {TypeC} TypeConstructor The constructor of the type definition. E.g. Y.Text, Y.Array, Y.Map, ...
* @return {InstanceType<TypeC>} The created type. Constructed with TypeConstructor
*
* @public
*/
@@ -227,6 +234,7 @@ export class Doc extends ObservableV2 {
return t
})
const Constr = type.constructor
// @ts-ignore
if (TypeConstructor !== AbstractType && Constr !== TypeConstructor) {
if (Constr === AbstractType) {
// @ts-ignore
@@ -245,12 +253,12 @@ export class Doc extends ObservableV2 {
t._length = type._length
this.share.set(name, t)
t._integrate(this, null)
return /** @type {InstanceType<Type>} */ (t)
return /** @type {InstanceType<TypeC>} */ (t)
} else {
throw new Error(`Type with the name ${name} has already been defined with a different constructor`)
}
}
return /** @type {InstanceType<Type>} */ (type)
return /** @type {InstanceType<TypeC>} */ (type)
}
/**
@@ -261,7 +269,7 @@ export class Doc extends ObservableV2 {
* @public
*/
getArray (name = '') {
return /** @type {YArray<T>} */ (this.get(name, YArray))
return /** @type {YArray<any>} */ (this.get(name, YArray))
}
/**

View File

@@ -1,140 +0,0 @@
import {
YArray,
YMap,
readIdSet,
writeIdSet,
createIdSet,
mergeIdSets,
IdSetEncoderV1, DSDecoderV1, ID, IdSet, YArrayEvent, Transaction, Doc // eslint-disable-line
} from '../internals.js'
import * as decoding from 'lib0/decoding'
export class PermanentUserData {
/**
* @param {Doc} doc
* @param {YMap<any>} [storeType]
*/
constructor (doc, storeType = doc.getMap('users')) {
/**
* @type {Map<string,IdSet>}
*/
const dss = new Map()
this.yusers = storeType
this.doc = doc
/**
* Maps from clientid to userDescription
*
* @type {Map<number,string>}
*/
this.clients = new Map()
this.dss = dss
/**
* @param {YMap<any>} user
* @param {string} userDescription
*/
const initUser = (user, userDescription) => {
/**
* @type {YArray<Uint8Array>}
*/
const ds = user.get('ds')
const ids = user.get('ids')
const addClientId = /** @param {number} clientid */ clientid => this.clients.set(clientid, userDescription)
ds.observe(/** @param {YArrayEvent<any>} event */ event => {
event.changes.added.forEach(item => {
item.content.getContent().forEach(encodedDs => {
if (encodedDs instanceof Uint8Array) {
this.dss.set(userDescription, mergeIdSets([this.dss.get(userDescription) || createIdSet(), readIdSet(new DSDecoderV1(decoding.createDecoder(encodedDs)))]))
}
})
})
})
this.dss.set(userDescription, mergeIdSets(ds.map(encodedDs => readIdSet(new DSDecoderV1(decoding.createDecoder(encodedDs))))))
ids.observe(/** @param {YArrayEvent<any>} event */ event =>
event.changes.added.forEach(item => item.content.getContent().forEach(addClientId))
)
ids.forEach(addClientId)
}
// observe users
storeType.observe(event => {
event.keysChanged.forEach(userDescription =>
initUser(storeType.get(userDescription), userDescription)
)
})
// add initial data
storeType.forEach(initUser)
}
/**
* @param {Doc} doc
* @param {number} clientid
* @param {string} userDescription
* @param {Object} conf
* @param {function(Transaction, IdSet):boolean} [conf.filter]
*/
setUserMapping (doc, clientid, userDescription, { filter = () => true } = {}) {
const users = this.yusers
let user = users.get(userDescription)
if (!user) {
user = new YMap()
user.set('ids', new YArray())
user.set('ds', new YArray())
users.set(userDescription, user)
}
user.get('ids').push([clientid])
users.observe(_event => {
setTimeout(() => {
const userOverwrite = users.get(userDescription)
if (userOverwrite !== user) {
// user was overwritten, port all data over to the next user object
// @todo Experiment with Y.Sets here
user = userOverwrite
// @todo iterate over old type
this.clients.forEach((_userDescription, clientid) => {
if (userDescription === _userDescription) {
user.get('ids').push([clientid])
}
})
const encoder = new IdSetEncoderV1()
const ds = this.dss.get(userDescription)
if (ds) {
writeIdSet(encoder, ds)
user.get('ds').push([encoder.toUint8Array()])
}
}
}, 0)
})
doc.on('afterTransaction', /** @param {Transaction} transaction */ transaction => {
setTimeout(() => {
const yds = user.get('ds')
const ds = transaction.deleteSet
if (transaction.local && ds.clients.size > 0 && filter(transaction, ds)) {
const encoder = new IdSetEncoderV1()
writeIdSet(encoder, ds)
yds.push([encoder.toUint8Array()])
}
})
})
}
/**
* @param {number} clientid
* @return {any}
*/
getUserByClientId (clientid) {
return this.clients.get(clientid) || null
}
/**
* @param {ID} id
* @return {string | null}
*/
getUserByDeletedId (id) {
for (const [userDescription, ds] of this.dss.entries()) {
if (ds.hasId(id)) {
return userDescription
}
}
return null
}
}

View File

@@ -153,7 +153,7 @@ export const createRelativePosition = (type, item, assoc) => {
/**
* Create a relativePosition based on a absolute position.
*
* @param {AbstractType<any>} type The base type (e.g. YText or YArray).
* @param {AbstractType} type The base type (e.g. YText or YArray).
* @param {number} index The absolute position.
* @param {number} [assoc]
* @param {import('../utils/AttributionManager.js').AbstractAttributionManager} attributionManager

View File

@@ -84,13 +84,13 @@ export class Transaction {
* All types that were directly modified (property added or child
* inserted/deleted). New types are not included in this Set.
* Maps from type to parentSubs (`item.parentSub = null` for YArray)
* @type {Map<AbstractType<YEvent<any>>,Set<String|null>>}
* @type {Map<import('../utils/types.js').YType,Set<String|null>>}
*/
this.changed = new Map()
/**
* Stores the events for the types that observe also child elements.
* It is mainly used by `observeDeep`.
* @type {Map<AbstractType<YEvent<any>>,Array<YEvent<any>>>}
* @type {Map<import('../utils/types.js').YType,Array<YEvent<any>>>}
*/
this.changedParentTypes = new Map()
/**
@@ -198,7 +198,7 @@ export const nextID = transaction => {
* did not change, it was just added and we should not fire events for `type`.
*
* @param {Transaction} transaction
* @param {AbstractType<YEvent<any>>} type
* @param {import('../utils/types.js').YType} type
* @param {string|null} parentSub
*/
export const addChangedTypeToTransaction = (transaction, type, parentSub) => {

View File

@@ -37,7 +37,7 @@ export class StackItem {
*/
const clearUndoManagerStackItem = (tr, um, stackItem) => {
iterateStructsByIdSet(tr, stackItem.deletions, item => {
if (item instanceof Item && um.scope.some(type => type === tr.doc || isParentOf(/** @type {AbstractType<any>} */ (type), item))) {
if (item instanceof Item && um.scope.some(type => type === tr.doc || isParentOf(/** @type {import('../utils/types.js').YType} */ (type), item))) {
keepItem(item, false)
}
})
@@ -79,7 +79,7 @@ const popStackItem = (undoManager, stack, eventType) => {
}
struct = item
}
if (!struct.deleted && scope.some(type => type === transaction.doc || isParentOf(/** @type {AbstractType<any>} */ (type), /** @type {Item} */ (struct)))) {
if (!struct.deleted && scope.some(type => type === transaction.doc || isParentOf(/** @type {import('../utils/types.js').YType} */ (type), /** @type {Item} */ (struct)))) {
itemsToDelete.push(struct)
}
}
@@ -87,7 +87,7 @@ const popStackItem = (undoManager, stack, eventType) => {
iterateStructsByIdSet(transaction, stackItem.deletions, struct => {
if (
struct instanceof Item &&
scope.some(type => type === transaction.doc || isParentOf(/** @type {AbstractType<any>} */ (type), struct)) &&
scope.some(type => type === transaction.doc || isParentOf(/** @type {import('../utils/types.js').YType} */ (type), struct)) &&
// Never redo structs in stackItem.insertions because they were created and deleted in the same capture interval.
!stackItem.insertions.hasId(struct.id)
) {
@@ -143,7 +143,7 @@ const popStackItem = (undoManager, stack, eventType) => {
* @property {StackItem} StackItemEvent.stackItem
* @property {any} StackItemEvent.origin
* @property {'undo'|'redo'} StackItemEvent.type
* @property {Map<AbstractType<YEvent<any>>,Array<YEvent<any>>>} StackItemEvent.changedParentTypes
* @property {Map<import('../utils/types.js').YType,Array<YEvent<any>>>} StackItemEvent.changedParentTypes
*/
/**
@@ -157,7 +157,7 @@ const popStackItem = (undoManager, stack, eventType) => {
*/
export class UndoManager extends ObservableV2 {
/**
* @param {Doc|AbstractType<any>|Array<AbstractType<any>>} typeScope Limits the scope of the UndoManager. If this is set to a ydoc instance, all changes on that ydoc will be undone. If set to a specific type, only changes on that type or its children will be undone. Also accepts an array of types.
* @param {Doc|import('../utils/types.js').YType|Array<import('../utils/types.js').YType>} typeScope Limits the scope of the UndoManager. If this is set to a ydoc instance, all changes on that ydoc will be undone. If set to a specific type, only changes on that type or its children will be undone. Also accepts an array of types.
* @param {UndoManagerOptions} options
*/
constructor (typeScope, {
@@ -170,7 +170,7 @@ export class UndoManager extends ObservableV2 {
} = {}) {
super()
/**
* @type {Array<AbstractType<any> | Doc>}
* @type {Array<import('../utils/types.js').YType | Doc>}
*/
this.scope = []
this.doc = doc
@@ -210,7 +210,7 @@ export class UndoManager extends ObservableV2 {
// Only track certain transactions
if (
!this.captureTransaction(transaction) ||
!this.scope.some(type => transaction.changedParentTypes.has(/** @type {AbstractType<any>} */ (type)) || type === this.doc) ||
!this.scope.some(type => transaction.changedParentTypes.has(/** @type {import('../utils/types.js').YType} */ (type)) || type === this.doc) ||
(!this.trackedOrigins.has(transaction.origin) && (!transaction.origin || !this.trackedOrigins.has(transaction.origin.constructor)))
) {
return
@@ -242,7 +242,7 @@ export class UndoManager extends ObservableV2 {
}
// make sure that deleted structs are not gc'd
iterateStructsByIdSet(transaction, transaction.deleteSet, /** @param {Item|GC} item */ item => {
if (item instanceof Item && this.scope.some(type => type === transaction.doc || isParentOf(/** @type {AbstractType<any>} */ (type), item))) {
if (item instanceof Item && this.scope.some(type => type === transaction.doc || isParentOf(/** @type {import('../utils/types.js').YType} */ (type), item))) {
keepItem(item, true)
}
})
@@ -265,7 +265,7 @@ export class UndoManager extends ObservableV2 {
/**
* Extend the scope.
*
* @param {Array<AbstractType<any> | Doc> | AbstractType<any> | Doc} ytypes
* @param {Array<import('../utils/types.js').YType | Doc> | import('../utils/types.js').YType | Doc} ytypes
*/
addToScope (ytypes) {
const tmpSet = new Set(this.scope)

View File

@@ -1,31 +1,39 @@
import {
TextDeltaBuilder, Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line
diffIdSet,
mergeIdSets,
noAttributionsManager,
AbstractAttributionManager, Item, AbstractType, Transaction, AbstractStruct // eslint-disable-line
} from '../internals.js'
import * as set from 'lib0/set'
import * as array from 'lib0/array'
import * as error from 'lib0/error'
import * as delta from 'lib0/delta' // eslint-disable-line
/**
* @typedef {import('./types.js').YType} _YType
*/
const errorComputeChanges = 'You must not compute changes after the event-handler fired.'
/**
* @template {AbstractType<any>} T
* @template {_YType} Target
* YEvent describes the changes on a YType.
*/
export class YEvent {
/**
* @param {T} target The changed type.
* @param {Target} target The changed type.
* @param {Transaction} transaction
* @param {Set<any>?} subs The keys that changed
*/
constructor (target, transaction) {
constructor (target, transaction, subs) {
/**
* The type on which this event was created on.
* @type {T}
* @type {Target}
*/
this.target = target
/**
* The current target on which the observe callback is called.
* @type {AbstractType<any>}
* @type {_YType}
*/
this.currentTarget = target
/**
@@ -42,13 +50,31 @@ export class YEvent {
*/
this._keys = null
/**
* @type {import('./Delta.js').TextDelta<any,undefined>?}
* @type {(Target extends AbstractType<infer D,any> ? D : delta.Delta<any,any,any,any,any>)|null}
*/
this._delta = null
/**
* @type {Array<string|number>|null}
*/
this._path = null
/**
* Whether the children changed.
* @type {Boolean}
* @private
*/
this.childListChanged = false
/**
* Set of all changed attributes.
* @type {Set<string>}
*/
this.keysChanged = new Set()
subs?.forEach((sub) => {
if (sub === null) {
this.childListChanged = true
} else {
this.keysChanged.add(sub)
}
})
}
/**
@@ -90,6 +116,7 @@ export class YEvent {
}
const keys = new Map()
const target = this.target
// @ts-ignore
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
changed.forEach(key => {
if (key !== null) {
@@ -136,18 +163,6 @@ export class YEvent {
return this._keys
}
/**
* This is a computed property. Note that this can only be safely computed during the
* event call. Computing this property after other changes happened might result in
* unexpected behavior (incorrect computation of deltas). A safe way to collect changes
* is to store the `changes` or the `delta` object. Avoid storing the `transaction` object.
*
* @type {import('./Delta.js').Delta}
*/
get delta () {
return this.changes.delta
}
/**
* Check if a struct is added by this event.
*
@@ -161,77 +176,25 @@ export class YEvent {
}
/**
* This is a computed property. Note that this can only be safely computed during the
* event call. Computing this property after other changes happened might result in
* unexpected behavior (incorrect computation of deltas). A safe way to collect changes
* is to store the `changes` or the `delta` object. Avoid storing the `transaction` object.
* @param {AbstractAttributionManager} am
* @return {Target extends AbstractType<infer D,any> ? D : delta.Delta<any,any,any,any>} The Delta representation of this type.
*
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:import('./Delta.js').Delta}}
* @public
*/
get changes () {
let changes = this._changes
if (changes === null) {
if (this.transaction.doc._transactionCleanups.length === 0) {
throw error.create(errorComputeChanges)
}
const target = this.target
const added = set.create()
const deleted = set.create()
/**
* @type {Array<{insert:Array<any>}|{delete:number}|{retain:number}>}
*/
const delta = []
changes = {
added,
deleted,
delta,
keys: this.keys
}
const changed = /** @type Set<string|null> */ (this.transaction.changed.get(target))
if (changed.has(null)) {
/**
* @type {any}
*/
let lastOp = null
const packOp = () => {
if (lastOp) {
delta.push(lastOp)
}
}
for (let item = target._start; item !== null; item = item.right) {
if (item.deleted) {
if (this.deletes(item) && !this.adds(item)) {
if (lastOp === null || lastOp.delete === undefined) {
packOp()
lastOp = { delete: 0 }
}
lastOp.delete += item.length
deleted.add(item)
} // else nop
} else {
if (this.adds(item)) {
if (lastOp === null || lastOp.insert === undefined) {
packOp()
lastOp = { insert: [] }
}
lastOp.insert = lastOp.insert.concat(item.content.getContent())
added.add(item)
} else {
if (lastOp === null || lastOp.retain === undefined) {
packOp()
lastOp = { retain: 0 }
}
lastOp.retain += item.length
}
}
}
if (lastOp !== null && lastOp.retain === undefined) {
packOp()
}
}
this._changes = changes
}
return /** @type {any} */ (changes)
getDelta (am = noAttributionsManager) {
const itemsToRender = mergeIdSets([diffIdSet(this.transaction.insertSet, this.transaction.deleteSet), diffIdSet(this.transaction.deleteSet, this.transaction.insertSet)])
return /** @type {any} */ (this.target.getContent(am, { itemsToRender, retainDeletes: true, renderAttrs: this.keysChanged, renderChildren: this.childListChanged }))
}
/**
* Compute the changes in the delta format.
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
*
* @type {Target extends AbstractType<infer D,any> ? D : delta.Delta<any,any,any,any,any>} The Delta representation of this type.
* @public
*/
get delta () {
return /** @type {any} */ (this._delta ?? (this._delta = this.getDelta()))
}
}
@@ -245,8 +208,8 @@ export class YEvent {
* console.log(path) // might look like => [2, 'key1']
* child === type.get(path[0]).get(path[1])
*
* @param {AbstractType<any>} parent
* @param {AbstractType<any>} child target
* @param {_YType} parent
* @param {_YType} child target
* @return {Array<string|number>} Path to the target
*
* @private
@@ -261,7 +224,7 @@ const getPathTo = (parent, child) => {
} else {
// parent is array-ish
let i = 0
let c = /** @type {AbstractType<any>} */ (child._item.parent)._start
let c = /** @type {import('../utils/types.js').YType} */ (child._item.parent)._start
while (c !== child._item && c !== null) {
if (!c.deleted && c.countable) {
i += c.length
@@ -270,7 +233,7 @@ const getPathTo = (parent, child) => {
}
path.unshift(i)
}
child = /** @type {AbstractType<any>} */ (child._item.parent)
child = /** @type {_YType} */ (child._item.parent)
}
return path
}

View File

@@ -501,6 +501,9 @@ export const writeStateAsUpdate = (encoder, doc, targetStateVector = new Map())
export const encodeStateAsUpdateV2 = (doc, encodedTargetStateVector = new Uint8Array([0]), encoder = new UpdateEncoderV2()) => {
const targetStateVector = decodeStateVector(encodedTargetStateVector)
writeStateAsUpdate(encoder, doc, targetStateVector)
/**
* @type {Uint8Array<ArrayBufferLike>[]}
*/
const updates = [encoder.toUint8Array()]
// also add the pending updates (if there are any)
if (doc.store.pendingDs) {

View File

@@ -3,7 +3,7 @@ import { AbstractType, Item } from '../internals.js' // eslint-disable-line
/**
* Check if `parent` is a parent of `child`.
*
* @param {AbstractType<any>} parent
* @param {import('../utils/types.js').YType} parent
* @param {Item|null} child
* @return {Boolean} Whether `parent` is a parent of `child`.
*

View File

@@ -5,7 +5,27 @@
* |import('../index.js').Map<any>
* |import('../index.js').Text<any>
* |import('../index.js').XmlElement<any>
* |import('../index.js').XmlFragment
* |import('../index.js').XmlFragment<any>
* |import('../index.js').XmlText
* |import('../index.js').XmlHook} YValue
*/
/**
* @typedef {import('../types/YArray.js').YArray<any>
* | import('../types/YMap.js').YMap<any>
* | import('../types/YText.js').YText<any>
* | import('../types/YXmlFragment.js').YXmlFragment<any,any>
* | import('../types/YXmlElement.js').YXmlElement<any,any>
* | import('../types/YXmlHook.js').YXmlHook
* | import('../types/YXmlText.js').YXmlText} YType
*/
/**
* @typedef {typeof import('../types/YArray.js').YArray<any>
* | typeof import('../types/YMap.js').YMap<any>
* | typeof import('../types/YText.js').YText<any>
* | typeof import('../types/YXmlFragment.js').YXmlFragment<any,any>
* | typeof import('../types/YXmlElement.js').YXmlElement<any,any>
* | typeof import('../types/YXmlHook.js').YXmlHook
* | typeof import('../types/YXmlText.js').YXmlText} YTypeConstructors
*/

View File

@@ -7,7 +7,7 @@
import * as Y from '../src/index.js'
import * as t from 'lib0/testing'
import * as delta from '../src/utils/Delta.js'
import * as delta from 'lib0/delta'
/**
* @param {t.TestCase} _tc
@@ -40,11 +40,11 @@ export const testAttributedEvents = _tc => {
})
const am = Y.createAttributionManagerFromDiff(v1, ydoc)
const c1 = ytext.getContent(am)
t.compare(c1, delta.createTextDelta().insert('hello ').insert('world', null, { delete: [] }))
t.compare(c1, delta.text().insert('hello ').insert('world', null, { delete: [] }))
let calledObserver = false
ytext.observe(event => {
const d = event.getDelta(am)
t.compare(d, delta.createTextDelta().retain(11).insert('!', null, { insert: [] }))
t.compare(d, delta.text().retain(11).insert('!', null, { insert: [] }))
calledObserver = true
})
ytext.insert(11, '!')
@@ -64,8 +64,8 @@ export const testInsertionsMindingAttributedContent = _tc => {
})
const am = Y.createAttributionManagerFromDiff(v1, ydoc)
const c1 = ytext.getContent(am)
t.compare(c1, delta.createTextDelta().insert('hello ').insert('world', null, { delete: [] }))
ytext.applyDelta(delta.createTextDelta().retain(11).insert('content'), am)
t.compare(c1, delta.text().insert('hello ').insert('world', null, { delete: [] }))
ytext.applyDelta(delta.text().retain(11).insert('content'), am)
t.assert(ytext.toString() === 'hello content')
}
@@ -82,7 +82,7 @@ export const testInsertionsIntoAttributedContent = _tc => {
})
const am = Y.createAttributionManagerFromDiff(v1, ydoc)
const c1 = ytext.getContent(am)
t.compare(c1, delta.createTextDelta().insert('hello ').insert('word', null, { insert: [] }))
ytext.applyDelta(delta.createTextDelta().retain(9).insert('l'), am)
t.compare(c1, delta.text().insert('hello ').insert('word', null, { insert: [] }))
ytext.applyDelta(delta.text().retain(9).insert('l'), am)
t.assert(ytext.toString() === 'hello world')
}

File diff suppressed because one or more lines are too long

View File

@@ -1,227 +0,0 @@
import * as t from 'lib0/testing'
import * as delta from '../src/utils/Delta.js'
import * as Y from 'yjs'
import * as schema from 'lib0/schema'
/**
* @param {t.TestCase} _tc
*/
export const testDelta = _tc => {
const d = delta.createTextDelta().insert('hello').insert(' ').useAttributes({ bold: true }).insert('world').useAttribution({ insert: ['tester'] }).insert('!').done()
t.compare(d.toJSON(), [{ insert: 'hello ' }, { insert: 'world', attributes: { bold: true } }, { insert: '!', attributes: { bold: true }, attribution: { insert: ['tester'] } }])
}
/**
* @param {t.TestCase} _tc
*/
export const testDeltaMerging = _tc => {
const d = delta.createTextDelta()
.insert('hello')
.insert('world')
.insert(' ', { italic: true })
.insert({})
.insert([1])
.insert([2])
.done()
t.compare(d.toJSON(), [{ insert: 'helloworld' }, { insert: ' ', attributes: { italic: true } }, { insert: {} }, { insert: [1, 2] }])
}
/**
* @param {t.TestCase} _tc
*/
export const testUseAttributes = _tc => {
const d = delta.createTextDelta()
.insert('a')
.updateUsedAttributes('bold', true)
.insert('b')
.insert('c', { bold: 4 })
.updateUsedAttributes('bold', null)
.insert('d')
.useAttributes({ italic: true })
.insert('e')
.useAttributes(null)
.insert('f')
.done()
const d2 = delta.createTextDelta()
.insert('a')
.insert('b', { bold: true })
.insert('c', { bold: 4 })
.insert('d')
.insert('e', { italic: true })
.insert('f')
.done()
t.compare(d, d2)
}
/**
* @param {t.TestCase} _tc
*/
export const testUseAttribution = _tc => {
const d = delta.createTextDelta()
.insert('a')
.updateUsedAttribution('insert', ['me'])
.insert('b')
.insert('c', null, { insert: ['you'] })
.updateUsedAttribution('insert', null)
.insert('d')
.useAttribution({ insert: ['me'] })
.insert('e')
.useAttribution(null)
.insert('f')
.done()
const d2 = delta.createTextDelta()
.insert('a')
.insert('b', null, { insert: ['me'] })
.insert('c', null, { insert: ['you'] })
.insert('d')
.insert('e', null, { insert: ['me'] })
.insert('f')
.done()
t.compare(d, d2)
}
/**
* @param {t.TestCase} _tc
*/
export const testMapDelta = _tc => {
const d = /** @type {delta.MapDeltaBuilder<{ key: string, v: number, over: string }>} */ (delta.createMapDelta())
d.set('key', 'value')
.useAttribution({ delete: ['me'] })
.delete('v', 94)
.useAttribution(null)
.set('over', 'andout', 'i existed before')
.done()
t.compare(d.toJSON(), {
key: { type: 'insert', value: 'value', prevValue: undefined, attribution: null },
v: { type: 'delete', prevValue: 94, attribution: { delete: ['me'] } },
over: { type: 'insert', value: 'andout', prevValue: 'i existed before', attribution: null }
})
t.compare(d.origin, null)
t.compare(d.remote, false)
t.compare(d.isDiff, true)
d.forEach((change, key) => {
if (key === 'v') {
t.assert(d.get(key)?.prevValue === 94) // should know that value is number
t.assert(change.prevValue === 94)
} else if (key === 'key') {
t.assert(d.get(key)?.value === 'value') // show know that value is a string
t.assert(change.value === 'value')
} else if (key === 'over') {
t.assert(change.value === 'andout')
} else {
throw new Error()
}
})
for (const [key, change] of d) {
if (key === 'v') {
t.assert(d.get(key)?.prevValue === 94)
t.assert(change.prevValue === 94) // should know that value is number
} else if (key === 'key') {
t.assert(change.value === 'value') // should know that value is string
} else if (key === 'over') {
t.assert(change.value === 'andout')
} else {
throw new Error()
}
}
}
/**
* @param {t.TestCase} _tc
*/
export const testXmlDelta = _tc => {
const d = /** @type {delta.XmlDelta<string, string, { a: 1 }>} */ (delta.createXmlDelta())
d.children.insert(['hi'])
d.attributes.set('a', 1)
d.attributes.delete('a', 1)
/**
* @type {Array<Array<string>| number>}
*/
const arr = []
d.children.forEach(
(op, index) => {
if (op instanceof delta.InsertArrayOp) {
arr.push(op.insert, index)
}
},
(op, index) => {
arr.push(op.insert, index)
},
(op, _index) => {
arr.push(op.retain)
},
(op, _index) => {
arr.push(op.delete)
}
)
t.compare(arr, [['hi'], 0, ['hi'], 0])
const x = d.done()
console.log(x)
}
const textDeltaSchema = schema.object({
ops: schema.array(
schema.any
)
})
/**
* @param {t.TestCase} _tc
*/
export const testTextModifyingDelta = _tc => {
const d = /** @type {delta.TextDelta<Y.Map<any>|Y.Array<any>,undefined>} */ (delta.createTextDelta().insert('hi').insert(new Y.Map()).done())
schema.assert(d, textDeltaSchema)
console.log(d)
}
/**
* @param {t.TestCase} _tc
*/
export const testYtypeDeltaTypings = _tc => {
const ydoc = new Y.Doc({ gc: false })
{
const yarray = /** @type {Y.Array<Y.Text|number>} */ (ydoc.getArray('numbers'))
const content = yarray.getContent()
content.forEach(
op => {
schema.union(
schema.constructedBy(delta.InsertArrayOp),
schema.constructedBy(delta.RetainOp),
schema.constructedBy(delta.DeleteOp)
).ensure(op)
},
op => {
schema.constructedBy(delta.InsertArrayOp).ensure(op)
},
op => {
schema.constructedBy(delta.RetainOp).ensure(op)
},
op => {
schema.constructedBy(delta.DeleteOp).ensure(op)
}
)
const cdeep = yarray.getContentDeep()
cdeep.forEach(
op => {
schema.union(
schema.constructedBy(delta.InsertArrayOp),
schema.constructedBy(delta.RetainOp),
schema.constructedBy(delta.DeleteOp),
schema.constructedBy(delta.ModifyOp)
).ensure(op)
},
op => {
schema.constructedBy(delta.InsertArrayOp).ensure(op)
},
op => {
schema.constructedBy(delta.RetainOp).ensure(op)
},
op => {
schema.constructedBy(delta.DeleteOp).ensure(op)
},
op => {
schema.constructedBy(delta.ModifyOp).ensure(op)
}
)
}
}

View File

@@ -1,5 +1,4 @@
import * as t from 'lib0/testing'
import * as promise from 'lib0/promise'
import {
contentRefs,
@@ -11,11 +10,7 @@ import {
readContentType,
readContentFormat,
readContentAny,
readContentDoc,
Doc,
PermanentUserData,
encodeStateAsUpdate,
applyUpdate
readContentDoc
} from '../src/internals.js'
import * as Y from '../src/index.js'
@@ -37,34 +32,6 @@ export const testStructReferences = tc => {
// contentRefs[10] is reserved for Skip structs
}
/**
* There is some custom encoding/decoding happening in PermanentUserData.
* This is why it landed here.
*
* @param {t.TestCase} tc
*/
export const testPermanentUserData = async tc => {
const ydoc1 = new Doc()
const ydoc2 = new Doc()
const pd1 = new PermanentUserData(ydoc1)
const pd2 = new PermanentUserData(ydoc2)
pd1.setUserMapping(ydoc1, ydoc1.clientID, 'user a')
pd2.setUserMapping(ydoc2, ydoc2.clientID, 'user b')
ydoc1.getText().insert(0, 'xhi')
ydoc1.getText().delete(0, 1)
ydoc2.getText().insert(0, 'hxxi')
ydoc2.getText().delete(1, 2)
await promise.wait(10)
applyUpdate(ydoc2, encodeStateAsUpdate(ydoc1))
applyUpdate(ydoc1, encodeStateAsUpdate(ydoc2))
// now sync a third doc with same name as doc1 and then create PermanentUserData
const ydoc3 = new Doc()
applyUpdate(ydoc3, encodeStateAsUpdate(ydoc1))
const pd3 = new PermanentUserData(ydoc3)
pd3.setUserMapping(ydoc3, ydoc3.clientID, 'user a')
}
/**
* Reported here: https://github.com/yjs/yjs/issues/308
* @param {t.TestCase} tc

View File

@@ -11,7 +11,6 @@ import * as doc from './doc.tests.js'
import * as snapshot from './snapshot.tests.js'
import * as updates from './updates.tests.js'
import * as relativePositions from './relativePositions.tests.js'
import * as delta from './delta.tests.js'
import * as idset from './IdSet.tests.js'
import * as idmap from './IdMap.tests.js'
import * as attribution from './attribution.tests.js'
@@ -25,7 +24,7 @@ if (isBrowser) {
}
const tests = {
doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot, updates, relativePositions, delta, idset, idmap, attribution
doc, map, array, text, xml, encoding, undoredo, compatibility, snapshot, updates, relativePositions, idset, idmap, attribution
}
const run = async () => {

View File

@@ -2,7 +2,7 @@ import * as Y from './testHelper.js'
import * as t from 'lib0/testing'
import * as prng from 'lib0/prng'
import * as math from 'lib0/math'
import * as delta from '../src/utils/Delta.js'
import * as delta from 'lib0/delta'
import { createIdMapFromIdSet, noAttributionsManager, TwosetAttributionManager, createAttributionManagerFromSnapshots } from 'yjs/internals'
const { init, compare } = Y
@@ -13,22 +13,19 @@ const { init, compare } = Y
* @param {t.TestCase} _tc
*/
export const testDeltaBug = _tc => {
const initialDelta = [{
attributes: {
'block-id': 'block-28eea923-9cbb-4b6f-a950-cf7fd82bc087'
},
insert: '\n'
},
{
attributes: {
const initialDelta = delta.create()
.insert('\n', {
attributes: {
'block-id': 'block-28eea923-9cbb-4b6f-a950-cf7fd82bc087'
},
insert: '\n'
})
.insert('\n\n\n', {
'table-col': {
width: '150'
}
},
insert: '\n\n\n'
},
{
attributes: {
})
.insert('\n', {
'block-id': 'block-9144be72-e528-4f91-b0b2-82d20408e9ea',
'table-cell-line': {
rowspan: '1',
@@ -40,11 +37,8 @@ export const testDeltaBug = _tc => {
cell: 'cell-apba4k',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
})
.insert('\n', {
'block-id': 'block-639adacb-1516-43ed-b272-937c55669a1c',
'table-cell-line': {
rowspan: '1',
@@ -56,11 +50,8 @@ export const testDeltaBug = _tc => {
cell: 'cell-a8qf0r',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
})
.insert('\n', {
'block-id': 'block-6302ca4a-73a3-4c25-8c1e-b542f048f1c6',
'table-cell-line': {
rowspan: '1',
@@ -72,11 +63,8 @@ export const testDeltaBug = _tc => {
cell: 'cell-oi9ikb',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
})
.insert('\n', {
'block-id': 'block-ceeddd05-330e-4f86-8017-4a3a060c4627',
'table-cell-line': {
rowspan: '1',
@@ -88,11 +76,8 @@ export const testDeltaBug = _tc => {
cell: 'cell-dt6ks2',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
})
.insert('\n', {
'block-id': 'block-37b19322-cb57-4e6f-8fad-0d1401cae53f',
'table-cell-line': {
rowspan: '1',
@@ -104,11 +89,8 @@ export const testDeltaBug = _tc => {
cell: 'cell-qah2ay',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
})
.insert('\n', {
'block-id': 'block-468a69b5-9332-450b-9107-381d593de249',
'table-cell-line': {
rowspan: '1',
@@ -120,11 +102,8 @@ export const testDeltaBug = _tc => {
cell: 'cell-fpcz5a',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
})
.insert('\n', {
'block-id': 'block-26b1d252-9b2e-4808-9b29-04e76696aa3c',
'table-cell-line': {
rowspan: '1',
@@ -136,11 +115,8 @@ export const testDeltaBug = _tc => {
cell: 'cell-zrhylp',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
})
.insert('\n', {
'block-id': 'block-6af97ba7-8cf9-497a-9365-7075b938837b',
'table-cell-line': {
rowspan: '1',
@@ -152,11 +128,8 @@ export const testDeltaBug = _tc => {
cell: 'cell-s1q9nt',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
})
.insert('\n', {
'block-id': 'block-107e273e-86bc-44fd-b0d7-41ab55aca484',
'table-cell-line': {
rowspan: '1',
@@ -168,56 +141,22 @@ export const testDeltaBug = _tc => {
cell: 'cell-20b0j9',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
})
.insert('\n', {
'block-id': 'block-38161f9c-6f6d-44c5-b086-54cc6490f1e3'
},
insert: '\n'
},
{
insert: 'Content after table'
},
{
attributes: {
})
.insert('Content after table')
.insert('\n', {
'block-id': 'block-15630542-ef45-412d-9415-88f0052238ce'
},
insert: '\n'
}
]
})
const ydoc1 = new Y.Doc()
const ytext = ydoc1.getText()
ytext.applyDelta(initialDelta)
const addingDash = [
{
retain: 12
},
{
insert: '-'
}
]
const addingDash = delta.create().retain(12).insert('-')
ytext.applyDelta(addingDash)
const addingSpace = [
{
retain: 13
},
{
insert: ' '
}
]
const addingSpace = delta.create().retain(13).insert(' ')
ytext.applyDelta(addingSpace)
const addingList = [
{
retain: 12
},
{
delete: 2
},
{
retain: 1,
attributes: {
const addingList = delta.create().retain(12).delete(2).retain(1, {
// Clear table line attribute
'table-cell-line': null,
// Add list attribute in place of table-cell-line
@@ -228,15 +167,10 @@ export const testDeltaBug = _tc => {
cell: 'cell-20b0j9',
list: 'bullet'
}
}
}
]
})
ytext.applyDelta(addingList)
const result = ytext.getContent()
/**
* @type {delta.TextDelta<any,any>}
*/
const expectedResult = delta.createTextDelta()
const expectedResult = delta.text()
.insert('\n', { 'block-id': 'block-28eea923-9cbb-4b6f-a950-cf7fd82bc087' })
.insert('\n\n\n', { 'table-col': { width: '150' } })
.insert('\n', {
@@ -366,7 +300,6 @@ export const testDeltaBug = _tc => {
.insert('\n', {
'block-id': 'block-15630542-ef45-412d-9415-88f0052238ce'
})
.done()
t.compare(result, expectedResult)
}

View File

@@ -1,7 +1,7 @@
import * as Y from '../src/index.js'
import { init, compare } from './testHelper.js'
import * as t from 'lib0/testing'
import * as delta from '../src/utils/Delta.js'
import * as delta from 'lib0/delta'
export const testCustomTypings = () => {
const ydoc = new Y.Doc()
@@ -99,25 +99,6 @@ export const testEvents = tc => {
compare(users)
}
/**
* @param {t.TestCase} tc
*/
export const testTreewalker = tc => {
const { users, xml0 } = init(tc, { users: 3 })
const paragraph1 = new Y.XmlElement('p')
const paragraph2 = new Y.XmlElement('p')
const text1 = new Y.XmlText('init')
const text2 = new Y.XmlText('text')
paragraph1.insert(0, [text1, text2])
xml0.insert(0, [paragraph1, paragraph2, new Y.XmlElement('img')])
const allParagraphs = xml0.querySelectorAll('p')
t.assert(allParagraphs.length === 2, 'found exactly two paragraphs')
t.assert(allParagraphs[0] === paragraph1, 'querySelectorAll found paragraph1')
t.assert(allParagraphs[1] === paragraph2, 'querySelectorAll found paragraph2')
t.assert(xml0.querySelector('p') === paragraph1, 'querySelector found paragraph1')
compare(users)
}
/**
* @param {t.TestCase} _tc
*/
@@ -125,7 +106,7 @@ export const testYtextAttributes = _tc => {
const ydoc = new Y.Doc()
const ytext = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
ytext.observe(event => {
t.compare(event.changes.keys.get('test'), { action: 'add', oldValue: undefined })
t.assert(event.delta.attrs.get('test')?.type === 'insert')
})
ytext.setAttribute('test', 42)
t.compare(ytext.getAttribute('test'), 42)
@@ -201,13 +182,12 @@ export const testClone = _tc => {
export const testFormattingBug = _tc => {
const ydoc = new Y.Doc()
const yxml = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText))
const delta = [
{ insert: 'A', attributes: { em: {}, strong: {} } },
{ insert: 'B', attributes: { em: {} } },
{ insert: 'C', attributes: { em: {}, strong: {} } }
]
yxml.applyDelta(delta)
t.compare(yxml.getContent().toJSON(), delta)
const q = delta.create()
.insert('A', { em: {}, strong: {} })
.insert('B', { em: {} })
.insert('C', { em: {}, strong: {} })
yxml.applyDelta(q)
t.compare(yxml.getContent(), q)
}
/**
@@ -243,11 +223,11 @@ export const testFragmentAttributedContent = _tc => {
yfragment.delete(0, 1)
yfragment.insert(1, [elem3])
})
const expectedContent = delta.createArrayDelta().insert([elem1], null, { delete: [] }).insert([elem2]).insert([elem3], null, { insert: [] })
const expectedContent = delta.create().insert([elem1], null, { delete: [] }).insert([elem2]).insert([elem3], null, { insert: [] })
const attributedContent = yfragment.getContent(attributionManager)
console.log(attributedContent.children.toJSON())
t.assert(attributedContent.children.equals(expectedContent))
t.compare(elem1.getContent(attributionManager).toJSON(), delta.createTextDelta().insert('hello', null, { delete: [] }).done().toJSON())
console.log(attributedContent.toJSON())
t.assert(attributedContent.equals(expectedContent))
t.compare(elem1.getContent(attributionManager).toJSON(), delta.create().insert('hello', null, { delete: [] }).toJSON())
})
}
@@ -272,29 +252,29 @@ export const testElementAttributedContent = _tc => {
yelement.insert(1, [elem3])
yelement.setAttribute('key', '42')
})
const expectedContent = delta.createArrayDelta().insert([elem1], null, { delete: [] }).insert([elem2]).insert([elem3], null, { insert: [] })
const expectedContent = delta.create().insert([elem1], null, { delete: [] }).insert([elem2]).insert([elem3], null, { insert: [] })
const attributedContent = yelement.getContent(attributionManager)
console.log('children', attributedContent.children.toJSON())
console.log('attributes', attributedContent.attributes)
t.assert(attributedContent.children.equals(expectedContent))
t.compare(attributedContent.attributes.toJSON(), { key: { type: 'insert', prevValue: undefined, value: '42', attribution: { insert: [] } } })
console.log('children', attributedContent.toJSON())
console.log('attributes', attributedContent)
t.assert(attributedContent.equals(expectedContent))
t.compare(attributedContent.toJSON().attrs, { key: { type: 'insert', prevValue: undefined, value: '42', attribution: { insert: [] } } })
t.group('test getContentDeep', () => {
const expectedContent = delta.createArrayDelta().insert(
[delta.createTextDelta().insert('hello', null, { delete: [] })],
const expectedContent = delta.create().insert(
[delta.text().insert('hello', null, { delete: [] })],
null,
{ delete: [] }
).insert([delta.createXmlDelta('span')])
).insert([delta.create('span')])
.insert([
delta.createTextDelta().insert('world', null, { insert: [] })
delta.text().insert('world', null, { insert: [] })
], null, { insert: [] })
const attributedContent = yelement.getContentDeep(attributionManager)
console.log('children', JSON.stringify(attributedContent.children.toJSON(), null, 2))
console.log('children', JSON.stringify(attributedContent.toJSON().children, null, 2))
console.log('cs expec', JSON.stringify(expectedContent.toJSON(), null, 2))
console.log('attributes', attributedContent.attributes)
t.assert(attributedContent.children.equals(expectedContent))
t.compare(attributedContent.attributes, /** @type {delta.MapDeltaBuilder<any>} */ (delta.createMapDelta()).set('key', '42', undefined, { insert: [] }))
t.compare(attributedContent.attributes.toJSON(), { key: { type: 'insert', prevValue: undefined, value: '42', attribution: { insert: [] } } })
t.assert(attributedContent.nodeName === 'UNDEFINED')
console.log('attributes', attributedContent.toJSON().attrs)
t.assert(attributedContent.equals(expectedContent))
t.compare(attributedContent, /** @type {delta.MapDelta<any>} */ (delta.map()).set('key', '42', { insert: [] }))
t.compare(attributedContent.toJSON().attrs, { key: { type: 'insert', prevValue: undefined, value: '42', attribution: { insert: [] } } })
t.assert(attributedContent.name === 'UNDEFINED')
})
})
}
@@ -316,64 +296,64 @@ export const testElementAttributedContentViaDiffer = _tc => {
yelement.setAttribute('key', '42')
})
const attributionManager = Y.createAttributionManagerFromDiff(ydocV1, ydoc)
const expectedContent = delta.createArrayDelta().insert([delta.createTextDelta().insert('hello')], null, { delete: [] }).insert([elem2.getContentDeep()]).insert([delta.createTextDelta().insert('world', null, { insert: [] })], null, { insert: [] })
const expectedContent = delta.create().insert([delta.create().insert('hello')], null, { delete: [] }).insert([elem2.getContentDeep()]).insert([delta.create().insert('world', null, { insert: [] })], null, { insert: [] })
const attributedContent = yelement.getContentDeep(attributionManager)
console.log('children', attributedContent.children.toJSON())
console.log('attributes', attributedContent.attributes)
t.compare(attributedContent.children.toJSON(), expectedContent.toJSON())
t.assert(attributedContent.children.equals(expectedContent))
t.compare(attributedContent.attributes.toJSON(), { key: { type: 'insert', prevValue: undefined, value: '42', attribution: { insert: [] } } })
console.log('children', attributedContent.toJSON().children)
console.log('attributes', attributedContent.toJSON().attrs)
t.compare(attributedContent.toJSON(), expectedContent.toJSON())
t.assert(attributedContent.equals(expectedContent))
t.compare(attributedContent.toJSON().attrs, { key: { type: 'insert', prevValue: undefined, value: '42', attribution: { insert: [] } } })
t.group('test getContentDeep', () => {
const expectedContent = delta.createArrayDelta().insert(
[delta.createTextDelta().insert('hello')],
const expectedContent = delta.create().insert(
[delta.create().insert('hello')],
null,
{ delete: [] }
).insert([delta.createXmlDelta('span')])
).insert([delta.create('span')])
.insert([
delta.createTextDelta().insert('world', null, { insert: [] })
delta.create().insert('world', null, { insert: [] })
], null, { insert: [] })
const attributedContent = yelement.getContentDeep(attributionManager)
console.log('children', JSON.stringify(attributedContent.children.toJSON(), null, 2))
console.log('children', JSON.stringify(attributedContent.toJSON().children, null, 2))
console.log('cs expec', JSON.stringify(expectedContent.toJSON(), null, 2))
console.log('attributes', attributedContent.attributes)
t.assert(attributedContent.children.equals(expectedContent))
t.compare(attributedContent.attributes.toJSON(), { key: { type: 'insert', prevValue: undefined, value: '42', attribution: { insert: [] } } })
t.assert(attributedContent.nodeName === 'UNDEFINED')
console.log('attributes', attributedContent.toJSON().attrs)
t.assert(attributedContent.equals(expectedContent))
t.compare(attributedContent.toJSON().attrs, { key: { type: 'insert', prevValue: undefined, value: '42', attribution: { insert: [] } } })
t.assert(attributedContent.name === 'UNDEFINED')
})
ydoc.transact(() => {
elem3.insert(0, 'big')
})
t.group('test getContentDeep after some more updates', () => {
t.info('expecting diffingAttributionManager to auto update itself')
const expectedContent = delta.createArrayDelta().insert(
[delta.createTextDelta().insert('hello')],
const expectedContent = delta.create().insert(
[delta.create().insert('hello')],
null,
{ delete: [] }
).insert([delta.createXmlDelta('span')])
).insert([delta.create('span')])
.insert([
delta.createTextDelta().insert('bigworld', null, { insert: [] })
delta.create().insert('bigworld', null, { insert: [] })
], null, { insert: [] })
const attributedContent = yelement.getContentDeep(attributionManager)
console.log('children', JSON.stringify(attributedContent.children.toJSON(), null, 2))
console.log('children', JSON.stringify(attributedContent.toJSON().children, null, 2))
console.log('cs expec', JSON.stringify(expectedContent.toJSON(), null, 2))
console.log('attributes', attributedContent.attributes)
t.assert(attributedContent.children.equals(expectedContent))
t.compare(attributedContent.attributes.toJSON(), { key: { type: 'insert', prevValue: undefined, value: '42', attribution: { insert: [] } } })
t.assert(attributedContent.nodeName === 'UNDEFINED')
console.log('attributes', attributedContent.toJSON().attrs)
t.assert(attributedContent.equals(expectedContent))
t.compare(attributedContent.toJSON().attrs, { key: { type: 'insert', prevValue: undefined, value: '42', attribution: { insert: [] } } })
t.assert(attributedContent.name === 'UNDEFINED')
})
Y.applyUpdate(ydocV1, Y.encodeStateAsUpdate(ydoc))
t.group('test getContentDeep both docs synced', () => {
t.info('expecting diffingAttributionManager to auto update itself')
const expectedContent = delta.createArrayDelta().insert([delta.createXmlDelta('span')]).insert([
delta.createTextDelta().insert('bigworld')
const expectedContent = delta.create().insert([delta.create('span')]).insert([
delta.create().insert('bigworld')
])
const attributedContent = yelement.getContentDeep(attributionManager)
console.log('children', JSON.stringify(attributedContent.children.toJSON(), null, 2))
console.log('children', JSON.stringify(attributedContent.toJSON().children, null, 2))
console.log('cs expec', JSON.stringify(expectedContent.toJSON(), null, 2))
console.log('attributes', attributedContent.attributes)
t.assert(attributedContent.children.equals(expectedContent))
t.compare(attributedContent.attributes.toJSON(), { key: { type: 'insert', prevValue: undefined, value: '42', attribution: null } })
t.assert(attributedContent.nodeName === 'UNDEFINED')
console.log('attributes', attributedContent.toJSON().attrs)
t.assert(attributedContent.equals(expectedContent))
t.compare(attributedContent.toJSON().attrs, { key: { type: 'insert', prevValue: undefined, value: '42' } })
t.assert(attributedContent.name === 'UNDEFINED')
})
}

View File

@@ -19,5 +19,6 @@
"yjs/testHelper": ["./tests/testHelper.js"]
}
},
"include": ["./src/**/*.js", "./tests/**/*.js"]
"include": ["./src/**/*.js", "./tests/**/*.js"],
"exclude": ["../lib0/**"]
}