diff --git a/README.md b/README.md index e4ad0bea..15c1f219 100644 --- a/README.md +++ b/README.md @@ -743,8 +743,6 @@ or any of its children.
Clone this type into a fresh Yjs type.
toArray():Array<Y.XmlElement|Y.XmlText>
Copies the children to a new Array.
- toDOM():DocumentFragment -
Transforms this type and all children to new DOM elements.
toString():string
Get the XML serialization of all descendants.
toJSON():string @@ -818,8 +816,6 @@ content and be actually XML compliant.
Clone this type into a fresh Yjs type.
toArray():Array<Y.XmlElement|Y.XmlText>
Copies the children to a new Array.
- toDOM():Element -
Transforms this type and all children to a new DOM element.
toString():string
Get the XML serialization of all descendants.
toJSON():string diff --git a/package-lock.json b/package-lock.json index d124bcc9..0d6308d9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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": { diff --git a/package.json b/package.json index 20af3808..c40d18af 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/src/index.js b/src/index.js index e4c811e2..03ceba7c 100644 --- a/src/index.js +++ b/src/index.js @@ -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, diff --git a/src/internals.js b/src/internals.js index d487ce14..a67f6e83 100644 --- a/src/internals.js +++ b/src/internals.js @@ -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' diff --git a/src/structs/ContentType.js b/src/structs/ContentType.js index a69677d8..25ff3f67 100644 --- a/src/structs/ContentType.js +++ b/src/structs/ContentType.js @@ -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>} + * @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} type + * @param {YType_CT} type */ constructor (type) { /** - * @type {AbstractType} + * @type {YType_CT} */ this.type = type } diff --git a/src/structs/Item.js b/src/structs/Item.js index 0efc8e25..e4070304 100644 --- a/src/structs/Item.js +++ b/src/structs/Item.js @@ -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} */ (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} */ (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} */ (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} */ (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} */ (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} */ (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} */ (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} */ (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|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|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} */ (this.parent), this.parentSub) + addChangedTypeToTransaction(transaction, /** @type {import('../utils/types.js').YType} */ (this.parent), this.parentSub) if ((/** @type {AbstractType} */ (this.parent)._item !== null && /** @type {AbstractType} */ (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} */ (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 diff --git a/src/types/AbstractType.js b/src/types/AbstractType.js index 282704b9..bce269c6 100644 --- a/src/types/AbstractType.js +++ b/src/types/AbstractType.js @@ -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} 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} t + * @param {AbstractType} t * @return {Array} */ 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} type + * @param {import('../utils/types.js').YType} type * @param {Transaction} transaction - * @param {EventType} event + * @param {YEvent} 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} */ (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} [EventDelta=delta.Delta] + * @template {YType_} [Self=any] */ export class AbstractType { constructor () { @@ -284,7 +295,7 @@ export class AbstractType { this._length = 0 /** * Event handlers - * @type {EventHandler} + * @type {EventHandler,Transaction>} */ this._eH = createEventHandler() /** @@ -299,10 +310,18 @@ export class AbstractType { } /** - * @return {AbstractType|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} */ (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} + * @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} + * @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, 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,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?} [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>} + */ + 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 }} */ (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} */ - 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} 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} D + * @typedef {D extends delta.Delta + * ? delta.Delta< + * N, + * { [K in keyof Attrs]: TypeToDelta }, + * TypeToDelta, + * Text + * > + * : D + * } ToDeepEventDelta + */ + +/** + * @template {any} T + * @typedef {(Extract> extends AbstractType ? (unknown extends D ? never : ToDeepEventDelta) : never) | Exclude>} TypeToDelta + */ + +/** + * @param {AbstractType} type * @param {number} start * @param {number} end * @return {Array} @@ -465,7 +746,7 @@ export const typeListSlice = (type, start, end) => { } /** - * @param {AbstractType} type + * @param {import('../utils/types.js').YType} type * @return {Array} * * @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} TypeDelta - * @param {AbstractType} type + * @template {delta.Delta} 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>} */ @@ -526,11 +807,10 @@ export const typeListGetContent = (type, am) => { } } } - return /** @type {TypeDelta} */ (d.done()) } /** - * @param {AbstractType} type + * @param {AbstractType} type * @param {Snapshot} snapshot * @return {Array} * @@ -555,7 +835,7 @@ export const typeListToArraySnapshot = (type, snapshot) => { /** * Executes a provided function on once on every element of this YArray. * - * @param {AbstractType} type + * @param {AbstractType} 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} type - * @param {function(C,number,AbstractType):R} f + * @param {AbstractType} type + * @param {function(C,number,AbstractType):R} f * @return {Array} * * @private @@ -597,7 +877,7 @@ export const typeListMap = (type, f) => { } /** - * @param {AbstractType} type + * @param {AbstractType} type * @return {IterableIterator} * * @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} type - * @param {function(any,number,AbstractType):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} 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} parent + * @param {YType_} parent * @param {Item?} referenceItem - * @param {Array|Array|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} parent + * @param {YType_} parent * @param {number} index * @param {Array|Array|number|null|string|Uint8Array>} content * @@ -819,7 +1099,7 @@ export const typeListInsertGenerics = (transaction, parent, index, content) => { * the search marker. * * @param {Transaction} transaction - * @param {AbstractType} parent + * @param {YType_} parent * @param {Array|Array|number|null|string|Uint8Array>} content * * @private @@ -839,7 +1119,7 @@ export const typeListPushGenerics = (transaction, parent, content) => { /** * @param {Transaction} transaction - * @param {AbstractType} 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} parent + * @param {YType_} parent * @param {string} key * * @private @@ -901,9 +1181,9 @@ export const typeMapDelete = (transaction, parent, key) => { /** * @param {Transaction} transaction - * @param {AbstractType} parent + * @param {YType_} parent * @param {string} key - * @param {Object|number|null|Array|string|Uint8Array|AbstractType} 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} parent + * @param {YType_} parent * @param {string} key * @return {Object|number|null|Array|string|Uint8Array|AbstractType|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} parent + * @template {delta.Delta} TypeDelta + * @param {TypeDelta} d + * @param {YType_} parent + * @param {Set?} 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>} */ @@ -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>} @@ -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)) + } } /** diff --git a/src/types/YArray.js b/src/types/YArray.js index 54a10a54..23642c3b 100644 --- a/src/types/YArray.js +++ b/src/types/YArray.js @@ -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} 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> - */ -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} [TypeDelta=ArrayDelta] - * @template {T extends AbstractType ? ArrayDelta>|DeepD,DeepD> : ArrayDelta} [EventDeltaDeep=T extends AbstractType ? ArrayDelta>|DeepD,DeepD> : ArrayDelta] - * @extends AbstractType,TypeDelta,EventDeltaDeep> + * @extends {AbstractType,YArray>} * @implements {Iterable} */ 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} - */ - _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} + * @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>} 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} 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 diff --git a/src/types/YMap.js b/src/types/YMap.js index 7372ba93..f9ff292a 100644 --- a/src/types/YMap.js +++ b/src/types/YMap.js @@ -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> - * Event that describes the changes on a YMap. - */ -export class YMapEvent extends YEvent { - /** - * @param {YMap} ymap The YArray that changed. - * @param {Transaction} transaction - * @param {Set} 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> + * @extends AbstractType> * @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} - */ - _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} + * @return {this} */ clone () { - /** - * @type {YMap} - */ - 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} 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 diff --git a/src/types/YText.js b/src/types/YText.js index c2ba4b37..19fd305d 100644 --- a/src/types/YText.js +++ b/src/types/YText.js @@ -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} parent + * @param {import('../utils/types.js').YType} parent * @param {number} length * @param {Object} attributes * @@ -214,7 +202,7 @@ const findNextPosition = (transaction, pos, count) => { /** * @param {Transaction} transaction - * @param {AbstractType} 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} parent + * @param {import('../utils/types.js').YType} parent * @param {ItemTextListPosition} currPos * @param {Map} negatedAttributes * @@ -311,7 +299,7 @@ const minimizeAttributeChanges = (currPos, attributes) => { /** * @param {Transaction} transaction - * @param {AbstractType} parent + * @param {import('../utils/types.js').YType} parent * @param {ItemTextListPosition} currPos * @param {Object} attributes * @return {Map} @@ -341,9 +329,9 @@ const insertAttributes = (transaction, parent, currPos, attributes) => { /** * @param {Transaction} transaction - * @param {AbstractType} parent + * @param {import('../utils/types.js').YType} parent * @param {ItemTextListPosition} currPos - * @param {string|object|AbstractType} text + * @param {string|object|import('../utils/types.js').YType} text * @param {Object} attributes * * @private @@ -638,82 +626,6 @@ const deleteText = (transaction, currPos, length) => { * @typedef {Object} TextAttributes */ -/** - * @template {{ [key:string]: any } | AbstractType } TextEmbeds - * @extends YEvent> - * Event that describes the changes on a YText type. - */ -export class YTextEvent extends YEvent { - /** - * @param {YText} ytext - * @param {Transaction} transaction - * @param {Set} 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} - */ - this.keysChanged = new Set() - subs.forEach((sub) => { - if (sub === null) { - this.childListChanged = true - } else { - this.keysChanged.add(sub) - } - }) - } - - /** - * @type {{added:Set,deleted:Set,keys:Map,delta:delta.TextDelta}} - */ - get changes () { - if (this._changes === null) { - /** - * @type {{added:Set,deleted:Set,keys:Map,delta:delta.TextDelta}} - */ - 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} 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} - * - * @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} [Embeds={ [key:string]:any } | AbstractType] - * @extends AbstractType> + * @template {{ [key:string]:any } | import('../utils/types.js').YType} [Embeds={ [key:string]:any } | import('../utils/types.js').YType] + * @extends {AbstractType>} */ 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} - */ - _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} */ - 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} */ (this), transaction, parentSubs) + callTypeObservers(/** @type {YText} */ (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 | delta.TextDelta} delta The changes to apply on this element. + * @param {delta.TextDelta} 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} */ (/** @type {delta.TextDelta} */ (delta).ops instanceof Array ? /** @type {delta.TextDelta} */ (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} */ (this._pending).push(() => this.applyDelta(delta)) + /** @type {Array} */ (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 ? 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} The Delta representation of this type. - * - * @public - */ - getContent (am = noAttributionsManager, { itemsToRender = null, retainInserts = false, retainDeletes = false } = {}) { - /** - * @type {import('../utils/Delta.js').TextDeltaBuilder} - */ - 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>} - */ - 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 }} */ (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 diff --git a/src/types/YXmlElement.js b/src/types/YXmlElement.js index bd61f21f..24e2ffd3 100644 --- a/src/types/YXmlElement.js +++ b/src/types/YXmlElement.js @@ -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|string|Uint8Array|AbstractType} 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 */ 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} + * @return {this} */ clone () { - /** - * @type {YXmlElement} - */ - 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} */ (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]?: KV[Key]}} A JSON Object that describes the attributes. + * @return {{ [Key in Extract]?: 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>, attributes: import('./AbstractType.js').MapAttributedContent }} - * - * @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>} */ (e.getContentDeep(am)) : e), d.attributes, d.attribution) - : d - )) - /** - * @todo there is a Attributes type and a DeepAttributes type. - * @type {delta.MapDeltaBuilder} - */ - 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} [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 */ diff --git a/src/types/YXmlEvent.js b/src/types/YXmlEvent.js deleted file mode 100644 index f18a06e8..00000000 --- a/src/types/YXmlEvent.js +++ /dev/null @@ -1,39 +0,0 @@ -import { - YEvent, - YXmlText, YXmlElement, YXmlFragment, Transaction // eslint-disable-line -} from '../internals.js' - -/** - * @extends YEvent - * 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} 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} - */ - this.attributesChanged = new Set() - subs.forEach((sub) => { - if (sub === null) { - this.childListChanged = true - } else { - this.attributesChanged.add(sub) - } - }) - } -} diff --git a/src/types/YXmlFragment.js b/src/types/YXmlFragment.js index 403059b6..50677afb 100644 --- a/src/types/YXmlFragment.js +++ b/src/types/YXmlFragment.js @@ -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} - */ -export class YXmlTreeWalker { - /** - * @param {YXmlFragment | YXmlElement} root - * @param {function(AbstractType):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} 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} */ (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 + * @template {any} [Children=any] + * @template {{[K in string]:any}} [Attrs={}] + * @extends AbstractType> */ 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):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} 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} 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} [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} content The array of content + * @param {Array} 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> }} - */ - 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> }} - */ - getContentDeep (am) { - const { children: origChildren } = this.getContent(am) - /** - * @type {import('../utils/Delta.js').ArrayDeltaBuilderBuilder>} - */ - 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 diff --git a/src/types/YXmlHook.js b/src/types/YXmlHook.js index 1bf24846..13e49be2 100644 --- a/src/types/YXmlHook.js +++ b/src/types/YXmlHook.js @@ -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.} [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 diff --git a/src/types/YXmlText.js b/src/types/YXmlText.js index 7b5ac37c..8f66ad11 100644 --- a/src/types/YXmlText.js +++ b/src/types/YXmlText.js @@ -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} [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 += `` - } - 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() diff --git a/src/utils/AttributionManager.js b/src/utils/AttributionManager.js index 0dc20073..745bc841 100644 --- a/src/utils/AttributionManager.js +++ b/src/utils/AttributionManager.js @@ -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 diff --git a/src/utils/Delta.js b/src/utils/Delta.js deleted file mode 100644 index 4f367246..00000000 --- a/src/utils/Delta.js +++ /dev/null @@ -1,1007 +0,0 @@ -import * as object from 'lib0/object' -import * as map from 'lib0/map' -import * as fun from 'lib0/function' -import * as traits from 'lib0/traits' -import * as error from 'lib0/error' -import * as s from 'lib0/schema' -import { attributionJsonSchema } from './AttributionManager.js' - -/** - * @template {any} ArrayContent - * @template {object} Embeds - * @template {Delta|undefined} ModifyingDelta - * @typedef {InsertStringOp|InsertEmbedOp|InsertArrayOp|RetainOp|DeleteOp|(ModifyingDelta extends undefined ? never : ModifyOp)} DeltaOp - */ - -/** - * @template {object} Embeds - * @template {Delta|undefined} Modifiers - * @typedef {InsertStringOp|InsertEmbedOp|RetainOp|DeleteOp|(Modifiers extends undefined ? never : ModifyOp)} TextDeltaOp - */ - -/** - * @template ArrayContent - * @typedef {InsertArrayOp|RetainOp|DeleteOp} ArrayDeltaOp - */ - -/** - * @typedef {import('./AttributionManager.js').Attribution} Attribution_ - */ - -/** - * @typedef {{ [key: string]: any }} FormattingAttributes - */ - -/** - * @typedef {Array} DeltaJson - */ - -/** - * @typedef {{ insert: string|object, attributes?: { [key: string]: any }, attribution?: Attribution_ } | { delete: number } | { retain: number, attributes?: { [key:string]: any }, attribution?: Attribution_ } | { modify: object }} DeltaJsonOp - */ - -export class InsertStringOp { - /** - * @param {string} insert - * @param {FormattingAttributes|null} attributes - * @param {Attribution_|null} attribution - */ - constructor (insert, attributes, attribution) { - this.insert = insert - this.attributes = attributes - this.attribution = attribution - } - - /** - * @return {'insert'} - */ - get type () { - return 'insert' - } - - get length () { - return (this.insert.constructor === Array || this.insert.constructor === String) ? this.insert.length : 1 - } - - /** - * @return {DeltaJsonOp} - */ - toJSON () { - return object.assign({ insert: this.insert }, this.attributes ? { attributes: this.attributes } : ({}), this.attribution ? { attribution: this.attribution } : ({})) - } - - /** - * @param {InsertStringOp} other - */ - [traits.EqualityTraitSymbol] (other) { - return fun.equalityDeep(this.insert, other.insert) && fun.equalityDeep(this.attributes, other.attributes) && fun.equalityDeep(this.attribution, other.attribution) - } -} - -/** - * @template {any} ArrayContent - */ -export class InsertArrayOp { - /** - * @param {Array} insert - * @param {FormattingAttributes|null} attributes - * @param {Attribution_|null} attribution - */ - constructor (insert, attributes, attribution) { - this.insert = insert - this.attributes = attributes - this.attribution = attribution - } - - /** - * @return {'insert'} - */ - get type () { - return 'insert' - } - - get length () { - return this.insert.length - } - - /** - * @return {DeltaJsonOp} - */ - toJSON () { - return object.assign({ insert: this.insert }, this.attributes ? { attributes: this.attributes } : ({}), this.attribution ? { attribution: this.attribution } : ({})) - } - - /** - * @param {InsertArrayOp} other - */ - [traits.EqualityTraitSymbol] (other) { - return fun.equalityDeep(this.insert, other.insert) && fun.equalityDeep(this.attributes, other.attributes) && fun.equalityDeep(this.attribution, other.attribution) - } -} - -/** - * @template {object} Embeds - */ -export class InsertEmbedOp { - /** - * @param {Embeds} insert - * @param {FormattingAttributes|null} attributes - * @param {Attribution_|null} attribution - */ - constructor (insert, attributes, attribution) { - this.insert = insert - this.attributes = attributes - this.attribution = attribution - } - - /** - * @return {'insertEmbed'} - */ - get type () { - return 'insertEmbed' - } - - get length () { - return 1 - } - - /** - * @return {DeltaJsonOp} - */ - toJSON () { - return object.assign({ insert: this.insert }, this.attributes ? { attributes: this.attributes } : ({}), this.attribution ? { attribution: this.attribution } : ({})) - } - - /** - * @param {InsertEmbedOp} other - */ - [traits.EqualityTraitSymbol] (other) { - return fun.equalityDeep(this.insert, other.insert) && fun.equalityDeep(this.attributes, other.attributes) && fun.equalityDeep(this.attribution, other.attribution) - } -} - -export class DeleteOp { - /** - * @param {number} len - */ - constructor (len) { - this.delete = len - } - - /** - * @return {'delete'} - */ - get type () { - return 'delete' - } - - get length () { - return 0 - } - - /** - * @return {DeltaJsonOp} - */ - toJSON () { - return { delete: this.delete } - } - - /** - * @param {DeleteOp} other - */ - [traits.EqualityTraitSymbol] (other) { - return this.delete === other.delete - } -} - -export class RetainOp { - /** - * @param {number} retain - * @param {FormattingAttributes|null} attributes - * @param {Attribution_|null} attribution - */ - constructor (retain, attributes, attribution) { - this.retain = retain - this.attributes = attributes - this.attribution = attribution - } - - /** - * @return {'retain'} - */ - get type () { - return 'retain' - } - - get length () { - return this.retain - } - - /** - * @return {DeltaJsonOp} - */ - toJSON () { - return object.assign({ retain: this.retain }, this.attributes ? { attributes: this.attributes } : {}, this.attribution ? { attribution: this.attribution } : {}) - } - - /** - * @param {RetainOp} other - */ - [traits.EqualityTraitSymbol] (other) { - return this.retain === other.retain && fun.equalityDeep(this.attributes, other.attributes) && fun.equalityDeep(this.attribution, other.attribution) - } -} - -/** - * Delta that can be applied on a YType Embed - * - * @template {Delta} DTypes - */ -export class ModifyOp { - /** - * @param {DTypes} delta - */ - constructor (delta) { - this.modify = delta - } - - /** - * @return {'modify'} - */ - get type () { - return 'modify' - } - - get length () { - return 1 - } - - /** - * @return {DeltaJsonOp} - */ - toJSON () { - return { modify: this.modify.toJSON() } - } - - /** - * @param {ModifyOp} other - */ - [traits.EqualityTraitSymbol] (other) { - return this.modify[traits.EqualityTraitSymbol](other.modify) - } -} - -export class AbstractDelta { - constructor () { - this.remote = false - /** - * @type {any} origin - */ - this.origin = null - this.isDiff = true - } - - /** - * @param {any} _other - */ - [traits.EqualityTraitSymbol] (_other) { - error.methodUnimplemented() - } -} - -/** - * @template {Delta|undefined} [Modifiers=any] - * @typedef {(TextDelta | ArrayDelta | MapDelta | XmlDelta )} Delta - */ - -/** - * @template {'array' | 'text' | 'custom'} Type - * @template {DeltaOp} TDeltaOp - * @template {Delta|undefined} Modifiers - */ -export class AbstractArrayDelta extends AbstractDelta { - /** - * @param {Type} type - */ - constructor (type) { - super() - this.type = type - /** - * @type {Array} - */ - this.ops = [] - } - - /** - * @template {(d:TDeltaOp) => DeltaOp} Mapper - * @param {Mapper} f - * @return {AbstractArrayDeltaBuilder infer OP ? OP : unknown,Modifiers>} - */ - map (f) { - const d = /** @type {AbstractArrayDeltaBuilder} */ (new /** @type {any} */ (this.constructor)(this.type)) - d.ops = this.ops.map(f) - // @ts-ignore - d.lastOp = d.ops[d.ops.length - 1] ?? null - return d - } - - /** - * - * Iterate through the changes. There are two approches to iterate through the changes. The - * following examples achieve the same thing: - * - * @example - * d.forEach((op, index) => { - * if (op instanceof delta.InsertArrayOp) { - * op.insert - * } else if (op instanceof delta.RetainOp ) { - * op.retain - * } else if (op instanceof delta.DeleteOp) { - * op.delete - * } - * }) - * - * The second approach doesn't require instanceof checks. - * - * @example - * d.forEach(null, - * (insertOp, index) => insertOp.insert, - * (retainOp, index) => insertOp.retain - * (deleteOp, index) => insertOp.delete - * ) - * - * @param {null|((d:TDeltaOp,index:number)=>void)} f - * @param {null|((insertOp:Exclude>,index:number)=>void)} insertHandler - * @param {null|((retainOp:RetainOp,index:number)=>void)} retainHandler - * @param {null|((deleteOp:DeleteOp,index:number)=>void)} deleteHandler - * @param {null|(Modifiers extends undefined ? null : ((modifyOp:ModifyOp,index:number)=>void))} modifyHandler - */ - forEach (f = null, insertHandler = null, retainHandler = null, deleteHandler = null, modifyHandler = null) { - for ( - let i = 0, index = 0, op = this.ops[i]; - i < this.ops.length; - i++, index += op.length, op = this.ops[i] - ) { - f?.(op, index) - switch (op.constructor) { - case RetainOp: - retainHandler?.(/** @type {RetainOp} */ (op), index) - break - case DeleteOp: - deleteHandler?.(/** @type {DeleteOp} */ (op), index) - break - case ModifyOp: - modifyHandler?.(/** @type {any}) */ (op), index) - break - default: - insertHandler?.(/** @type {any} */ (op), index) - } - } - } - - /** - * @param {AbstractArrayDelta} other - * @return {boolean} - */ - equals (other) { - return this[traits.EqualityTraitSymbol](other) - } - - /** - * @returns {DeltaJson} - */ - toJSON () { - return this.ops.map(o => o.toJSON()) - } - - /** - * @param {AbstractArrayDelta} other - */ - [traits.EqualityTraitSymbol] (other) { - return fun.equalityDeep(this.ops, other.ops) - } -} - -/** - * @template {object} Vals - * @template {keyof Vals} K - * @template {Delta|undefined} Modifiers - * @typedef {(change:MapDeltaChange,key:K) => void} MapDeltaChangeCallback - */ - -/** - * @template V - */ -class MapInsertOp { - /** - * @param {V} value - * @param {V|undefined} prevValue - * @param {Attribution_?} attribution - */ - constructor (value, prevValue, attribution) { - this.prevValue = prevValue - this.attribution = attribution - this.value = value - } - - /** - * @return {'insert'} - */ - get type () { return 'insert' } - - toJSON () { - return { - type: this.type, - value: this.value, - prevValue: this.prevValue, - attribution: this.attribution - } - } - - /** - * @param {MapInsertOp} other - */ - [traits.EqualityTraitSymbol] (other) { - return fun.equalityDeep(this.value, other.value) && fun.equalityDeep(this.prevValue, other.prevValue) && fun.equalityDeep(this.attribution, other.attribution) - } -} - -/** - * @template V - */ -class MapDeleteOp { - /** - * @param {V|undefined} prevValue - * @param {Attribution_?} attribution - */ - constructor (prevValue, attribution) { - this.prevValue = prevValue - this.attribution = attribution - } - - get value () { return undefined } - - /** - * @type {'delete'} - */ - get type () { return 'delete' } - - toJSON () { - return { - type: this.type, - prevValue: this.prevValue, - attribution: this.attribution - } - } - - /** - * @param {MapDeleteOp} other - */ - [traits.EqualityTraitSymbol] (other) { - return fun.equalityDeep(this.prevValue, other.prevValue) && fun.equalityDeep(this.attribution, other.attribution) - } -} - -/** - * @template {Delta} Modifiers - */ -class MapModifyOp { - /** - * @param {Modifiers} delta - */ - constructor (delta) { - this.modify = delta - } - - get value () { return undefined } - - /** - * @type {'modify'} - */ - get type () { return 'modify' } - - toJSON () { - return { - type: this.type, - modify: this.modify.toJSON() - } - } - - /** - * @param {MapModifyOp} other - */ - [traits.EqualityTraitSymbol] (other) { - return this.modify[traits.EqualityTraitSymbol](other.modify) - } -} - -/** - * @template V - * @template {Delta|undefined} Modifiers - * @typedef {MapInsertOp | MapDeleteOp | (Modifiers extends undefined ? never : MapModifyOp)} MapDeltaChange - */ - -export const mapDeltaChangeJsonSchema = s.union( - s.object({ type: s.literal('insert'), value: s.any, prevValue: s.any.optional, attribution: attributionJsonSchema.nullable.optional }), - s.object({ type: s.literal('delete'), prevValue: s.any.optional, attribution: attributionJsonSchema.nullable.optional }), - s.object({ type: s.literal('modify'), modify: s.any }) -) - -export const mapDeltaJsonSchema = s.record(s.string, mapDeltaChangeJsonSchema) - -/** - * @template {object} Vals - * @template {Delta|undefined} Modifiers - */ -export class MapDelta extends AbstractDelta { - constructor () { - super() - /** - * @type {Map>} - */ - this.changes = map.create() - /** - * @type {Attribution_?} - */ - this.usedAttribution = null - } - - /** - * - * Iterate through the changes. There are two approches to iterate through changes. The - * following two examples achieve the same thing: - * - * @example - * d.forEach((op, index) => { - * if (op instanceof delta.InsertArrayOp) { - * op.insert - * } else if (op instanceof delta.RetainOp ) { - * op.retain - * } else if (op instanceof delta.DeleteOp) { - * op.delete - * } else if (op instanceof delta.ModifyOp) { - * op.modify - * } - * }) - * - * The second approach doesn't require instanceof checks. - * - * @example - * d.forEach(null, - * (insertOp, index) => insertOp.insert, - * (retainOp, index) => insertOp.retain - * (deleteOp, index) => insertOp.delete - * (modifyOp, index) => insertOp.modify - * ) - * - * @param {null|((change:MapDeltaChange,key:keyof Vals)=>void)} changeHandler - * @param {null|((insertOp:MapInsertOp,key:keyof Vals)=>void)} insertHandler - * @param {null|((deleteOp:MapDeleteOp,key:keyof Vals)=>void)} deleteHandler - * @param {null|((modifyOp:(MapModifyOp),key:keyof Vals)=>void)} modifyHandler - */ - forEach (changeHandler = null, insertHandler = null, deleteHandler = null, modifyHandler = null) { - this.changes.forEach((change, key) => { - changeHandler?.(change, key) - switch (change.constructor) { - case MapDeleteOp: - deleteHandler?.(/** @type {MapDeleteOp} */ (change), key) - break - case MapInsertOp: - insertHandler?.(/** @type {MapInsertOp} */ (change), key) - break - case MapModifyOp: - modifyHandler?.(/** @type {MapModifyOp} */ (change), key) - break - } - }) - } - - /** - * @template {keyof Vals} K - * - * @param {K} key - * @return {MapDeltaChange | undefined} - */ - get (key) { - return /** @type {MapDeltaChange | undefined} */ (this.changes.get(key)) - } - - /** - * @param {keyof Vals} key - */ - has (key) { - return this.changes.has(key) - } - - /** - * @param {MapDelta} other - * @return {boolean} - */ - equals (other) { - return this[traits.EqualityTraitSymbol](other) - } - - /** - * @return {s.Unwrap} - */ - toJSON () { - /** - * @type {s.Unwrap} - */ - const changes = {} - this.changes.forEach((change, key) => { - changes[/** @type {string} */ (key)] = change.toJSON() - }) - return changes - } - - /** - * Preferred way to iterate through changes. - * - * @return {IterableIterator<{ [K in keyof Vals]: [K, MapDeltaChange] }[keyof Vals]>} - */ - [Symbol.iterator] () { - // @ts-ignore - return this.changes.entries() - } - - /** - * @param {MapDelta} other - */ - [traits.EqualityTraitSymbol] (other) { - return fun.equalityDeep(this.changes, other.changes) - } - - /** - * @return {MapDelta} - */ - done () { - return this - } -} - -/** - * @template {string|undefined} NodeName - * @template Children - * @template {object} Attrs - * @template {Delta|undefined} [ChildModifiers=undefined] - * @template {Delta|undefined} [AttrModifiers=undefined] - * @template {'done'|'mutable'} [Done='mutable'] - */ -export class XmlDelta extends AbstractDelta { - /** - * @param {NodeName} nodeName - * @param {ArrayDeltaBuilder} children - * @param {MapDelta} attributes - */ - constructor (nodeName, children, attributes) { - super() - this.nodeName = nodeName - /** - * @type {ArrayDeltaBuilder} - */ - this.children = children - /** - * @type {Done extends 'mutable' ? MapDeltaBuilder : MapDelta} - */ - this.attributes = /** @type {any} */ (attributes) - } - - toJSON () { - return { - nodeName: this.nodeName, - children: this.children.toJSON(), - attributes: this.attributes.toJSON() - } - } - - /** - * @return {XmlDelta} - */ - done () { - this.children.done() - this.attributes.done() - return /** @type {any} */ (this) - } - - /** - * @param {XmlDelta} other - */ - [traits.EqualityTraitSymbol] (other) { - return this.nodeName === other.nodeName && this.children[traits.EqualityTraitSymbol](other.children) && this.attributes[traits.EqualityTraitSymbol](other.attributes) - } -} - -/** - * @template {string|undefined} NodeName - * @template Children - * @template {object} Attrs - * @template {Delta|undefined} [ChildModifiers=undefined] - * @template {Delta|undefined} [AttrModifiers=undefined] - * @param {NodeName} nodeName - * @param {ArrayDeltaBuilder} children - * @param {MapDeltaBuilder} attributes - * @return {XmlDelta} - */ -export const createXmlDelta = (nodeName, children = createArrayDelta(), attributes = /** @type {any} */ (createMapDelta())) => new XmlDelta(nodeName, children, attributes) - -/** - * @template {object} Vals - * @template {Delta|undefined} [Modifiers=undefined] - * @extends MapDelta - */ -export class MapDeltaBuilder extends MapDelta { - /** - * @template {keyof Vals} K - * @param {K} key - * @param {Modifiers} delta - */ - modify (key, delta) { - this.changes.set(key, /** @type {any} */ ({ type: 'modify', delta })) - return this - } - - /** - * @template {keyof Vals} K - * @param {K} key - * @param {Vals[K]} newVal - * @param {Vals[K]|undefined} prevValue - * @param {Attribution_?} attribution - */ - set (key, newVal, prevValue = undefined, attribution = null) { - const mergedAttribution = mergeAttrs(this.usedAttribution, attribution) - this.changes.set(key, new MapInsertOp(newVal, prevValue, mergedAttribution)) - return this - } - - /** - * @template {keyof Vals} K - * @param {K} key - * @param {Vals[K]|undefined} prevValue - * @param {Attribution_?} attribution - */ - delete (key, prevValue = undefined, attribution = null) { - const mergedAttribution = mergeAttrs(this.usedAttribution, attribution) - this.changes.set(key, new MapDeleteOp(prevValue, mergedAttribution)) - return this - } - - /** - * @param {Attribution_?} attribution - */ - useAttribution (attribution) { - this.usedAttribution = attribution - return this - } -} - -export const createMapDelta = () => new MapDeltaBuilder() - -/** - * Helper function to merge attribution and attributes. The latter input "wins". - * - * @template {{ [key: string]: any }} T - * @param {T | null} a - * @param {T | null} b - */ -const mergeAttrs = (a, b) => object.isEmpty(a) ? b : (object.isEmpty(b) ? a : object.assign({}, a, b)) - -/** - * @template {'array' | 'text' | 'custom'} Type - * @template {DeltaOp} TDeltaOp - * @template {Delta|undefined} Modifiers - * @extends AbstractArrayDelta - */ -export class AbstractArrayDeltaBuilder extends AbstractArrayDelta { - /** - * @param {Type} type - */ - constructor (type) { - super(type) - /** - * @type {FormattingAttributes?} - */ - this.usedAttributes = null - /** - * @type {Attribution_?} - */ - this.usedAttribution = null - /** - * @type {TDeltaOp?} - */ - this.lastOp = null - } - - /** - * @param {FormattingAttributes?} attributes - * @return {this} - */ - useAttributes (attributes) { - this.usedAttributes = attributes - return this - } - - /** - * @param {string} name - * @param {any} value - */ - updateUsedAttributes (name, value) { - if (value == null) { - this.usedAttributes = object.assign({}, this.usedAttributes) - delete this.usedAttributes?.[name] - if (object.isEmpty(this.usedAttributes)) { - this.usedAttributes = null - } - } else if (!fun.equalityDeep(this.usedAttributes?.[name], value)) { - this.usedAttributes = object.assign({}, this.usedAttributes) - this.usedAttributes[name] = value - } - return this - } - - /** - * @template {keyof Attribution_} NAME - * @param {NAME} name - * @param {Attribution_[NAME]?} value - */ - updateUsedAttribution (name, value) { - if (value == null) { - this.usedAttribution = object.assign({}, this.usedAttribution) - delete this.usedAttribution?.[name] - if (object.isEmpty(this.usedAttribution)) { - this.usedAttribution = null - } - } else if (!fun.equalityDeep(this.usedAttribution?.[name], value)) { - this.usedAttribution = object.assign({}, this.usedAttribution) - this.usedAttribution[name] = value - } - return this - } - - /** - * @param {Attribution_?} attribution - */ - useAttribution (attribution) { - this.usedAttribution = attribution - return this - } - - /** - * @param {(TDeltaOp extends InsertStringOp ? string : never) | (TDeltaOp extends InsertEmbedOp ? (Embeds) : never) | (TDeltaOp extends InsertArrayOp ? Array : never) } insert - * @param {FormattingAttributes?} attributes - * @param {Attribution_?} attribution - * @return {this} - */ - insert (insert, attributes = null, attribution = null) { - const mergedAttributes = mergeAttrs(this.usedAttributes, attributes) - const mergedAttribution = mergeAttrs(this.usedAttribution, attribution) - if (((this.lastOp instanceof InsertStringOp && insert.constructor === String) || (this.lastOp instanceof InsertArrayOp && insert.constructor === Array)) && (mergedAttributes === this.lastOp.attributes || fun.equalityDeep(mergedAttributes, this.lastOp.attributes)) && (mergedAttribution === this.lastOp.attribution || fun.equalityDeep(mergedAttribution, this.lastOp.attribution))) { - // @ts-ignore - if (insert.constructor === String) { - // @ts-ignore - this.lastOp.insert += insert - } else { - // @ts-ignore - this.lastOp.insert.push(...insert) - } - } else { - const OpConstructor = /** @type {any} */ (insert.constructor === String ? InsertStringOp : (insert.constructor === Array ? InsertArrayOp : InsertEmbedOp)) - this.ops.push(this.lastOp = new OpConstructor(insert, object.isEmpty(mergedAttributes) ? null : mergedAttributes, object.isEmpty(mergedAttribution) ? null : mergedAttribution)) - } - return this - } - - /** - * @param {number} retain - * @param {FormattingAttributes?} attributes - * @param {Attribution_?} attribution - * @return {this} - */ - retain (retain, attributes = null, attribution = null) { - const mergedAttributes = mergeAttrs(this.usedAttributes, attributes) - const mergedAttribution = mergeAttrs(this.usedAttribution, attribution) - if (this.lastOp instanceof RetainOp && fun.equalityDeep(mergedAttributes, this.lastOp.attributes) && fun.equalityDeep(mergedAttribution, this.lastOp.attribution)) { - this.lastOp.retain += retain - } else { - // @ts-ignore - this.ops.push(this.lastOp = new RetainOp(retain, mergedAttributes, mergedAttribution)) - } - return this - } - - /** - * @param {number} len - * @return {this} - */ - delete (len) { - if (this.lastOp instanceof DeleteOp) { - this.lastOp.delete += len - } else { - // @ts-ignore - this.ops.push(this.lastOp = new DeleteOp(len)) - } - return this - } - - /** - * @return {Type extends 'array' ? ArrayDelta : (Type extends 'text' ? TextDelta : AbstractArrayDelta)} - */ - done () { - while (this.lastOp != null && this.lastOp instanceof RetainOp && this.lastOp.attributes === null && this.lastOp.attribution === null) { - this.ops.pop() - this.lastOp = this.ops[this.ops.length - 1] ?? null - } - return /** @type {any} */ (this) - } -} - -/** - * @template {any} ArrayContent - * @template {Delta|undefined} Modifiers - * @extends AbstractArrayDeltaBuilder<'array', ArrayDeltaOp,Modifiers> - */ -export class ArrayDeltaBuilder extends AbstractArrayDeltaBuilder { - constructor () { - super('array') - } -} - -/** - * @template {any} ArrayContent - * @template {Delta|undefined} Modifiers - * @typedef {AbstractArrayDelta<'array', ArrayDeltaOp,Modifiers>} ArrayDelta - */ - -/** - * @template {object} Embeds - * @template {Delta|undefined} Modifiers - * @typedef {AbstractArrayDelta<'text',TextDeltaOp,Modifiers>} TextDelta - */ - -/** - * @template {object} Embeds - * @template {Delta|undefined} [Modifiers=undefined] - * @extends AbstractArrayDeltaBuilder<'text',TextDeltaOp,Modifiers> - */ -export class TextDeltaBuilder extends AbstractArrayDeltaBuilder { - constructor () { - super('text') - } -} - -/** - * @template {object} [Embeds=any] - * @template {Delta|undefined} [Modifiers=undefined] - * @return {TextDeltaBuilder} - */ -export const createTextDelta = () => new TextDeltaBuilder() - -/** - * @template [V=any] - * @template {Delta|undefined} [Modifiers=undefined] - * @return {ArrayDeltaBuilder} - */ -export const createArrayDelta = () => new ArrayDeltaBuilder() - -/** - * @template {'custom' | 'text' | 'array'} T - * @param {DeltaJson} ops - * @param {T} type - */ -export const fromJSON = (ops, type) => { - const d = new AbstractArrayDeltaBuilder(type) - for (let i = 0; i < ops.length; i++) { - const op = /** @type {any} */ (ops[i]) - // @ts-ignore - if (op.insert !== undefined) { - d.insert(op.insert, op.attributes, op.attribution) - } else if (op.retain !== undefined) { - d.retain(op.retain, op.attributes ?? null, op.attribution ?? null) - } else if (op.delete !== undefined) { - d.delete(op.delete) - } else { - error.unexpectedCase() - } - } - return d.done() -} diff --git a/src/utils/Doc.js b/src/utils/Doc.js index c116f3f9..58590d34 100644 --- a/src/utils/Doc.js +++ b/src/utils/Doc.js @@ -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>>} + * @type {Map} */ 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} 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} 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} 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} */ (t) + return /** @type {InstanceType} */ (t) } else { throw new Error(`Type with the name ${name} has already been defined with a different constructor`) } } - return /** @type {InstanceType} */ (type) + return /** @type {InstanceType} */ (type) } /** @@ -261,7 +269,7 @@ export class Doc extends ObservableV2 { * @public */ getArray (name = '') { - return /** @type {YArray} */ (this.get(name, YArray)) + return /** @type {YArray} */ (this.get(name, YArray)) } /** diff --git a/src/utils/PermanentUserData.js b/src/utils/PermanentUserData.js deleted file mode 100644 index d4067fde..00000000 --- a/src/utils/PermanentUserData.js +++ /dev/null @@ -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} [storeType] - */ - constructor (doc, storeType = doc.getMap('users')) { - /** - * @type {Map} - */ - const dss = new Map() - this.yusers = storeType - this.doc = doc - /** - * Maps from clientid to userDescription - * - * @type {Map} - */ - this.clients = new Map() - this.dss = dss - /** - * @param {YMap} user - * @param {string} userDescription - */ - const initUser = (user, userDescription) => { - /** - * @type {YArray} - */ - 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} 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} 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 - } -} diff --git a/src/utils/RelativePosition.js b/src/utils/RelativePosition.js index ddee880c..c89aa266 100644 --- a/src/utils/RelativePosition.js +++ b/src/utils/RelativePosition.js @@ -153,7 +153,7 @@ export const createRelativePosition = (type, item, assoc) => { /** * Create a relativePosition based on a absolute position. * - * @param {AbstractType} 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 diff --git a/src/utils/Transaction.js b/src/utils/Transaction.js index f6b59095..ea34e571 100644 --- a/src/utils/Transaction.js +++ b/src/utils/Transaction.js @@ -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>,Set>} + * @type {Map>} */ this.changed = new Map() /** * Stores the events for the types that observe also child elements. * It is mainly used by `observeDeep`. - * @type {Map>,Array>>} + * @type {Map>>} */ 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>} type + * @param {import('../utils/types.js').YType} type * @param {string|null} parentSub */ export const addChangedTypeToTransaction = (transaction, type, parentSub) => { diff --git a/src/utils/UndoManager.js b/src/utils/UndoManager.js index 09cdc4b7..6857eb2f 100644 --- a/src/utils/UndoManager.js +++ b/src/utils/UndoManager.js @@ -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} */ (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} */ (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} */ (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>,Array>>} StackItemEvent.changedParentTypes + * @property {Map>>} StackItemEvent.changedParentTypes */ /** @@ -157,7 +157,7 @@ const popStackItem = (undoManager, stack, eventType) => { */ export class UndoManager extends ObservableV2 { /** - * @param {Doc|AbstractType|Array>} 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} 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 | Doc>} + * @type {Array} */ 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} */ (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} */ (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 | Doc> | AbstractType | Doc} ytypes + * @param {Array | import('../utils/types.js').YType | Doc} ytypes */ addToScope (ytypes) { const tmpSet = new Set(this.scope) diff --git a/src/utils/YEvent.js b/src/utils/YEvent.js index 37c32eae..082944dd 100644 --- a/src/utils/YEvent.js +++ b/src/utils/YEvent.js @@ -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} 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?} 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} + * @type {_YType} */ this.currentTarget = target /** @@ -42,13 +50,31 @@ export class YEvent { */ this._keys = null /** - * @type {import('./Delta.js').TextDelta?} + * @type {(Target extends AbstractType ? D : delta.Delta)|null} */ this._delta = null /** * @type {Array|null} */ this._path = null + /** + * Whether the children changed. + * @type {Boolean} + * @private + */ + this.childListChanged = false + /** + * Set of all changed attributes. + * @type {Set} + */ + 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 */ (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 ? D : delta.Delta} The Delta representation of this type. * - * @type {{added:Set,deleted:Set,keys:Map,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}|{delete:number}|{retain:number}>} - */ - const delta = [] - changes = { - added, - deleted, - delta, - keys: this.keys - } - const changed = /** @type Set */ (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 ? D : delta.Delta} 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} parent - * @param {AbstractType} child target + * @param {_YType} parent + * @param {_YType} child target * @return {Array} 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} */ (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} */ (child._item.parent) + child = /** @type {_YType} */ (child._item.parent) } return path } diff --git a/src/utils/encoding.js b/src/utils/encoding.js index 285e8259..d8bf4e87 100644 --- a/src/utils/encoding.js +++ b/src/utils/encoding.js @@ -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[]} + */ const updates = [encoder.toUint8Array()] // also add the pending updates (if there are any) if (doc.store.pendingDs) { diff --git a/src/utils/isParentOf.js b/src/utils/isParentOf.js index d8f5a613..e26d4eb1 100644 --- a/src/utils/isParentOf.js +++ b/src/utils/isParentOf.js @@ -3,7 +3,7 @@ import { AbstractType, Item } from '../internals.js' // eslint-disable-line /** * Check if `parent` is a parent of `child`. * - * @param {AbstractType} parent + * @param {import('../utils/types.js').YType} parent * @param {Item|null} child * @return {Boolean} Whether `parent` is a parent of `child`. * diff --git a/src/utils/types.js b/src/utils/types.js index 0901a6b6..a0414078 100644 --- a/src/utils/types.js +++ b/src/utils/types.js @@ -5,7 +5,27 @@ * |import('../index.js').Map * |import('../index.js').Text * |import('../index.js').XmlElement - * |import('../index.js').XmlFragment + * |import('../index.js').XmlFragment * |import('../index.js').XmlText * |import('../index.js').XmlHook} YValue */ + +/** + * @typedef {import('../types/YArray.js').YArray + * | import('../types/YMap.js').YMap + * | import('../types/YText.js').YText + * | import('../types/YXmlFragment.js').YXmlFragment + * | import('../types/YXmlElement.js').YXmlElement + * | import('../types/YXmlHook.js').YXmlHook + * | import('../types/YXmlText.js').YXmlText} YType + */ + +/** + * @typedef {typeof import('../types/YArray.js').YArray + * | typeof import('../types/YMap.js').YMap + * | typeof import('../types/YText.js').YText + * | typeof import('../types/YXmlFragment.js').YXmlFragment + * | typeof import('../types/YXmlElement.js').YXmlElement + * | typeof import('../types/YXmlHook.js').YXmlHook + * | typeof import('../types/YXmlText.js').YXmlText} YTypeConstructors + */ diff --git a/tests/attribution.tests.js b/tests/attribution.tests.js index 13bc896a..d44d26ce 100644 --- a/tests/attribution.tests.js +++ b/tests/attribution.tests.js @@ -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') } diff --git a/tests/compatibility.tests.js b/tests/compatibility.tests.js index 1155818e..513d347f 100644 --- a/tests/compatibility.tests.js +++ b/tests/compatibility.tests.js @@ -38,8 +38,8 @@ export const testMapDecodingCompatibilityV1 = _tc => { export const testTextDecodingCompatibilityV1 = _tc => { const oldDoc = 'BS8EAAUBBHRleHRveyJpbWFnZSI6Imh0dHBzOi8vdXNlci1pbWFnZXMuZ2l0aHVidXNlcmNvbnRlbnQuY29tLzU1NTM3NTcvNDg5NzUzMDctNjFlZmIxMDAtZjA2ZC0xMWU4LTkxNzctZWU4OTVlNTkxNmU1LnBuZyJ9RAQAATHBBAEEAAHBBAIEAAHEBAMEAAQxdXUKxQQCBANveyJpbWFnZSI6Imh0dHBzOi8vdXNlci1pbWFnZXMuZ2l0aHVidXNlcmNvbnRlbnQuY29tLzU1NTM3NTcvNDg5NzUzMDctNjFlZmIxMDAtZjA2ZC0xMWU4LTkxNzctZWU4OTVlNTkxNmU1LnBuZyJ9xQMJBAFveyJpbWFnZSI6Imh0dHBzOi8vdXNlci1pbWFnZXMuZ2l0aHVidXNlcmNvbnRlbnQuY29tLzU1NTM3NTcvNDg5NzUzMDctNjFlZmIxMDAtZjA2ZC0xMWU4LTkxNzctZWU4OTVlNTkxNmU1LnBuZyJ9xQMJBAlveyJpbWFnZSI6Imh0dHBzOi8vdXNlci1pbWFnZXMuZ2l0aHVidXNlcmNvbnRlbnQuY29tLzU1NTM3NTcvNDg5NzUzMDctNjFlZmIxMDAtZjA2ZC0xMWU4LTkxNzctZWU4OTVlNTkxNmU1LnBuZyJ9xgMBAwIGaXRhbGljBHRydWXGBAsDAgVjb2xvcgYiIzg4OCLEBAwDAgExxAQNAwIBMsEEDgMCAsYEEAMCBml0YWxpYwRudWxsxgQRAwIFY29sb3IEbnVsbMQDAQQLATHEBBMECwIyOcQEFQQLCzl6anpueXdvaHB4xAQgBAsIY25icmNhcQrBAxADEQHGAR8BIARib2xkBHRydWXGAgACAQRib2xkBG51bGzFAwkECm97ImltYWdlIjoiaHR0cHM6Ly91c2VyLWltYWdlcy5naXRodWJ1c2VyY29udGVudC5jb20vNTU1Mzc1Ny80ODk3NTMwNy02MWVmYjEwMC1mMDZkLTExZTgtOTE3Ny1lZTg5NWU1OTE2ZTUucG5nIn3GARABEQZpdGFsaWMEdHJ1ZcYELQERBWNvbG9yBiIjODg4IsYBEgETBml0YWxpYwRudWxsxgQvARMFY29sb3IEbnVsbMYCKwIsBGJvbGQEdHJ1ZcYCLQIuBGJvbGQEbnVsbMYCjAECjQEGaXRhbGljBHRydWXGAo4BAo8BBml0YWxpYwRudWxswQA2ADcBxgQ1ADcFY29sb3IGIiM4ODgixgNlA2YFY29sb3IEbnVsbMYDUwNUBGJvbGQEdHJ1ZcQEOANUFjEzMTZ6bHBrbWN0b3FvbWdmdGhicGfGBE4DVARib2xkBG51bGzGAk0CTgZpdGFsaWMEdHJ1ZcYEUAJOBWNvbG9yBiIjODg4IsYCTwJQBml0YWxpYwRudWxsxgRSAlAFY29sb3IEbnVsbMYChAEChQEGaXRhbGljBHRydWXGBFQChQEFY29sb3IGIiM4ODgixgKGAQKHAQZpdGFsaWMEbnVsbMYEVgKHAQVjb2xvcgRudWxsxAMpAyoRMTMyMWFwZ2l2eWRxc2pmc2XFBBIDAm97ImltYWdlIjoiaHR0cHM6Ly91c2VyLWltYWdlcy5naXRodWJ1c2VyY29udGVudC5jb20vNTU1Mzc1Ny80ODk3NTMwNy02MWVmYjEwMC1mMDZkLTExZTgtOTE3Ny1lZTg5NWU1OTE2ZTUucG5nIn0zAwAEAQR0ZXh0AjEyhAMBAzkwboQDBAF4gQMFAoQDBwJyCsQDBAMFBjEyOTd6bcQDDwMFAXbEAxADBQFwwQMRAwUBxAMSAwUFa3pxY2rEAxcDBQJzYcQDGQMFBHNqeQrBAxIDEwHBAAwAEAHEAA0ADgkxMzAyeGNpd2HEAygADgF5xAMpAA4KaGhlenVraXF0dMQDMwAOBWhudGsKxgMoAykEYm9sZAR0cnVlxAM5AykGMTMwNXJswQM/AykCxANBAykDZXlrxgNEAykEYm9sZARudWxsxAMzAzQJMTMwN3R2amllwQNOAzQCxANQAzQDamxoxANTAzQCZ3bEA1UDNAJsYsQDVwM0AmYKxgNBA0IEYm9sZARudWxswQNaA0ICxANcA0ICMDjBA14DQgLEA2ADQgEKxgNhA0IEYm9sZAR0cnVlxQIaAhtveyJpbWFnZSI6Imh0dHBzOi8vdXNlci1pbWFnZXMuZ2l0aHVidXNlcmNvbnRlbnQuY29tLzU1NTM3NTcvNDg5NzUzMDctNjFlZmIxMDAtZjA2ZC0xMWU4LTkxNzctZWU4OTVlNTkxNmU1LnBuZyJ9wQA3ADgCwQNlADgBxANmADgKMTVteml3YWJ6a8EDcAA4AsQDcgA4BnJybXNjdsEDeAA4AcQCYgJjATHEA3oCYwIzMsQDfAJjCTRyb3J5d3RoccQDhQECYwEKxAOFAQOGARkxMzI1aW9kYnppenhobWxpYnZweXJ4bXEKwQN6A3sBxgOgAQN7BWNvbG9yBiIjODg4IsYDfAN9Bml0YWxpYwRudWxsxgOiAQN9BWNvbG9yBG51bGxSAgAEAQR0ZXh0ATGEAgACMjiEAgIBOYECAwKEAgUBdYQCBgJ0Y4QCCAJqZYECCgKEAgwBaoECDQGBAg4BhAIPAnVmhAIRAQrEAg4CDwgxMjkycXJtZsQCGgIPAmsKxgIGAgcGaXRhbGljBHRydWXGAggCCQZpdGFsaWMEbnVsbMYCEQISBml0YWxpYwR0cnVlxAIfAhIBMcECIAISAsQCIgISAzRoc8QCJQISAXrGAiYCEgZpdGFsaWMEbnVsbMEAFQAWAsQCKQAWATDEAioAFgEwxAIrABYCaHjEAi0AFglvamVldHJqaHjBAjYAFgLEAjgAFgJrcsQCOgAWAXHBAjsAFgHBAjwAFgHEAj0AFgFuxAI+ABYCZQrGAiUCJgZpdGFsaWMEbnVsbMQCQQImAjEzwQJDAiYCxAJFAiYIZGNjeGR5eGfEAk0CJgJ6Y8QCTwImA2Fwb8QCUgImAnRuxAJUAiYBcsQCVQImAmduwQJXAiYCxAJZAiYBCsYCWgImBml0YWxpYwR0cnVlxAI6AjsEMTMwM8QCXwI7A3VodsQCYgI7BmdhbmxuCsUCVQJWb3siaW1hZ2UiOiJodHRwczovL3VzZXItaW1hZ2VzLmdpdGh1YnVzZXJjb250ZW50LmNvbS81NTUzNzU3LzQ4OTc1MzA3LTYxZWZiMTAwLWYwNmQtMTFlOC05MTc3LWVlODk1ZTU5MTZlNS5wbmcifcECPAI9AcECPgI/AcYDFwMYBml0YWxpYwR0cnVlxgJsAxgFY29sb3IGIiM4ODgixgMZAxoGaXRhbGljBG51bGzGAm4DGgVjb2xvcgRudWxswQMQBCkBxAJwBCkKMTMwOXpsZ3ZqeMQCegQpAWfBAnsEKQLGBA0EDgZpdGFsaWMEbnVsbMYCfgQOBWNvbG9yBG51bGzEAn8EDgUxMzEwZ8QChAEEDgJ3c8QChgEEDgZoeHd5Y2jEAowBBA4Ca3HEAo4BBA4Ec2RydcQCkgEEDgRqcWljwQKWAQQOBMQCmgEEDgEKxgKbAQQOBml0YWxpYwR0cnVlxgKcAQQOBWNvbG9yBiIjODg4IsECaAI7AcQCCgEBFjEzMThqd3NramFiZG5kcmRsbWphZQrGA1UDVgRib2xkBHRydWXGA1cDWARib2xkBG51bGzGAEAAQQZpdGFsaWMEdHJ1ZcYCtwEAQQRib2xkBG51bGzEArgBAEESMTMyNnJwY3pucWFob3BjcnRkxgLKAQBBBml0YWxpYwRudWxsxgLLAQBBBGJvbGQEdHJ1ZRkBAMUCAgIDb3siaW1hZ2UiOiJodHRwczovL3VzZXItaW1hZ2VzLmdpdGh1YnVzZXJjb250ZW50LmNvbS81NTUzNzU3LzQ4OTc1MzA3LTYxZWZiMTAwLWYwNmQtMTFlOC05MTc3LWVlODk1ZTU5MTZlNS5wbmcifcQCCgILBzEyOTN0agrGABgAGQRib2xkBHRydWXGAA0ADgRib2xkBG51bGxEAgAHMTMwNnJ1cMQBEAIAAnVqxAESAgANaWtrY2pucmNwc2Nrd8QBHwIAAQrFBBMEFG97ImltYWdlIjoiaHR0cHM6Ly91c2VyLWltYWdlcy5naXRodWJ1c2VyY29udGVudC5jb20vNTU1Mzc1Ny80ODk3NTMwNy02MWVmYjEwMC1mMDZkLTExZTgtOTE3Ny1lZTg5NWU1OTE2ZTUucG5nIn3FAx0DBW97ImltYWdlIjoiaHR0cHM6Ly91c2VyLWltYWdlcy5naXRodWJ1c2VyY29udGVudC5jb20vNTU1Mzc1Ny80ODk3NTMwNy02MWVmYjEwMC1mMDZkLTExZTgtOTE3Ny1lZTg5NWU1OTE2ZTUucG5nIn3GAlICUwRib2xkBHRydWXGAlQCVQRib2xkBG51bGzGAnsCfAZpdGFsaWMEdHJ1ZcYBJQJ8BWNvbG9yBiIjODg4IsYBJgJ8BGJvbGQEbnVsbMQBJwJ8CjEzMTRweWNhdnXGATECfAZpdGFsaWMEbnVsbMYBMgJ8BWNvbG9yBG51bGzBATMCfAHFADEAMm97ImltYWdlIjoiaHR0cHM6Ly91c2VyLWltYWdlcy5naXRodWJ1c2VyY29udGVudC5jb20vNTU1Mzc1Ny80ODk3NTMwNy02MWVmYjEwMC1mMDZkLTExZTgtOTE3Ny1lZTg5NWU1OTE2ZTUucG5nIn3GADUANgZpdGFsaWMEdHJ1ZcEANwA4AcQAMgAzEzEzMjJybmJhb2tvcml4ZW52cArEAgUCBhcxMzIzbnVjdnhzcWx6bndsZmF2bXBjCsYDDwMQBGJvbGQEdHJ1ZR0AAMQEAwQEDTEyOTVxZnJ2bHlmYXDEAAwEBAFjxAANBAQCanbBAAwADQHEABAADQEywQARAA0ExAAVAA0DZHZmxAAYAA0BYcYCAwIEBml0YWxpYwR0cnVlwQAaAgQCxAAcAgQEMDRrdcYAIAIEBml0YWxpYwRudWxsxQQgBCFveyJpbWFnZSI6Imh0dHBzOi8vdXNlci1pbWFnZXMuZ2l0aHVidXNlcmNvbnRlbnQuY29tLzU1NTM3NTcvNDg5NzUzMDctNjFlZmIxMDAtZjA2ZC0xMWU4LTkxNzctZWU4OTVlNTkxNmU1LnBuZyJ9xQJAABZveyJpbWFnZSI6Imh0dHBzOi8vdXNlci1pbWFnZXMuZ2l0aHVidXNlcmNvbnRlbnQuY29tLzU1NTM3NTcvNDg5NzUzMDctNjFlZmIxMDAtZjA2ZC0xMWU4LTkxNzctZWU4OTVlNTkxNmU1LnBuZyJ9xAQVBBYGMTMxMWtrxAIqAisIMTMxMnFyd3TEADECKwFixAAyAisDcnhxxAA1AisBasQANgIrAXjEADcCKwZkb3ZhbwrEAgAEKwMxMzHEAEAEKwkzYXhoa3RoaHXGAnoCewRib2xkBG51bGzFAEoCe297ImltYWdlIjoiaHR0cHM6Ly91c2VyLWltYWdlcy5naXRodWJ1c2VyY29udGVudC5jb20vNTU1Mzc1Ny80ODk3NTMwNy02MWVmYjEwMC1mMDZkLTExZTgtOTE3Ny1lZTg5NWU1OTE2ZTUucG5nIn3GAEsCewRib2xkBHRydWXEAl8CYBExMzE3cGZjeWhrc3JrcGt0CsQBHwQqCzEzMTliY2Nna3AKxAKSAQKTARUxMzIwY29oYnZjcmtycGpuZ2RvYwoFBAQCAg8CKQE1AQADEAESBBsCAwsGAhIBHgJAAk8CWwJfAmQDcQJ5AaABAQIOBAILAg4CIQIoAjcCPAJEAlgCagJwAXwClwEEngEBAQI0ATcB' // eslint-disable-next-line - const oldVal = [{"insert":"1306rup"},{"insert":"uj","attributes":{"italic":true,"color":"#888"}},{"insert":"ikkcjnrcpsckw1319bccgkp\n"},{"insert":"\n1131","attributes":{"bold":true}},{"insert":"1326rpcznqahopcrtd","attributes":{"italic":true}},{"insert":"3axhkthhu","attributes":{"bold":true}},{"insert":"28"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"9"},{"insert":"04ku","attributes":{"italic":true}},{"insert":"1323nucvxsqlznwlfavmpc\nu"},{"insert":"tc","attributes":{"italic":true}},{"insert":"je1318jwskjabdndrdlmjae\n1293tj\nj1292qrmf"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"k\nuf"},{"insert":"14hs","attributes":{"italic":true}},{"insert":"13dccxdyxg"},{"insert":"zc","attributes":{"italic":true,"color":"#888"}},{"insert":"apo"},{"insert":"tn","attributes":{"bold":true}},{"insert":"r"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"gn\n"},{"insert":"z","attributes":{"italic":true}},{"insert":"\n121"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"291311kk9zjznywohpx"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"cnbrcaq\n"},{"insert":"1","attributes":{"italic":true,"color":"#888"}},{"insert":"1310g"},{"insert":"ws","attributes":{"italic":true,"color":"#888"}},{"insert":"hxwych"},{"insert":"kq","attributes":{"italic":true}},{"insert":"sdru1320cohbvcrkrpjngdoc\njqic\n"},{"insert":"2","attributes":{"italic":true,"color":"#888"}},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"90n1297zm"},{"insert":"v1309zlgvjx","attributes":{"bold":true}},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"g","attributes":{"bold":true}},{"insert":"1314pycavu","attributes":{"italic":true,"color":"#888"}},{"insert":"pkzqcj"},{"insert":"sa","attributes":{"italic":true,"color":"#888"}},{"insert":"sjy\n"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"xr\n"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"1"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"1295qfrvlyfap201312qrwt"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"b1322rnbaokorixenvp\nrxq"},{"insert":"j","attributes":{"italic":true}},{"insert":"x","attributes":{"italic":true,"color":"#888"}},{"insert":"15mziwabzkrrmscvdovao\n0","attributes":{"italic":true}},{"insert":"hx","attributes":{"italic":true,"bold":true}},{"insert":"ojeetrjhxkr13031317pfcyhksrkpkt\nuhv1","attributes":{"italic":true}},{"insert":"32","attributes":{"italic":true,"color":"#888"}},{"insert":"4rorywthq1325iodbzizxhmlibvpyrxmq\n\nganln\nqne\n"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}},{"insert":"dvf"},{"insert":"ac","attributes":{"bold":true}},{"insert":"1302xciwa"},{"insert":"1305rl","attributes":{"bold":true}},{"insert":"08\n"},{"insert":"eyk","attributes":{"bold":true}},{"insert":"y1321apgivydqsjfsehhezukiqtt1307tvjiejlh"},{"insert":"1316zlpkmctoqomgfthbpg","attributes":{"bold":true}},{"insert":"gv"},{"insert":"lb","attributes":{"bold":true}},{"insert":"f\nhntk\njv1uu\n"},{"insert":{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}}] + const oldVal = [{"insert":"1306rup"},{"insert":"uj","attributes":{"italic":true,"color":"#888"}},{"insert":"ikkcjnrcpsckw1319bccgkp\n"},{"insert":"\n1131","attributes":{"bold":true}},{"insert":"1326rpcznqahopcrtd","attributes":{"italic":true}},{"insert":"3axhkthhu","attributes":{"bold":true}},{"insert":"28"},{"insert":[{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}]},{"insert":"9"},{"insert":"04ku","attributes":{"italic":true}},{"insert":"1323nucvxsqlznwlfavmpc\nu"},{"insert":"tc","attributes":{"italic":true}},{"insert":"je1318jwskjabdndrdlmjae\n1293tj\nj1292qrmf"},{"insert":[{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}]},{"insert":"k\nuf"},{"insert":"14hs","attributes":{"italic":true}},{"insert":"13dccxdyxg"},{"insert":"zc","attributes":{"italic":true,"color":"#888"}},{"insert":"apo"},{"insert":"tn","attributes":{"bold":true}},{"insert":"r"},{"insert":[{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}]},{"insert":"gn\n"},{"insert":"z","attributes":{"italic":true}},{"insert":"\n121"},{"insert":[{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}]},{"insert":"291311kk9zjznywohpx"},{"insert":[{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}]},{"insert":"cnbrcaq\n"},{"insert":"1","attributes":{"italic":true,"color":"#888"}},{"insert":"1310g"},{"insert":"ws","attributes":{"italic":true,"color":"#888"}},{"insert":"hxwych"},{"insert":"kq","attributes":{"italic":true}},{"insert":"sdru1320cohbvcrkrpjngdoc\njqic\n"},{"insert":"2","attributes":{"italic":true,"color":"#888"}},{"insert":[{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}]},{"insert":"90n1297zm"},{"insert":"v1309zlgvjx","attributes":{"bold":true}},{"insert":[{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}]},{"insert":"g","attributes":{"bold":true}},{"insert":"1314pycavu","attributes":{"italic":true,"color":"#888"}},{"insert":"pkzqcj"},{"insert":"sa","attributes":{"italic":true,"color":"#888"}},{"insert":"sjy\n"},{"insert":[{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}]},{"insert":"xr\n"},{"insert":[{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}]},{"insert":[{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}]},{"insert":[{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}]},{"insert":"1"},{"insert":[{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}]},{"insert":"1295qfrvlyfap201312qrwt"},{"insert":[{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}]},{"insert":"b1322rnbaokorixenvp\nrxq"},{"insert":"j","attributes":{"italic":true}},{"insert":"x","attributes":{"italic":true,"color":"#888"}},{"insert":"15mziwabzkrrmscvdovao\n0","attributes":{"italic":true}},{"insert":"hx","attributes":{"italic":true,"bold":true}},{"insert":"ojeetrjhxkr13031317pfcyhksrkpkt\nuhv1","attributes":{"italic":true}},{"insert":"32","attributes":{"italic":true,"color":"#888"}},{"insert":"4rorywthq1325iodbzizxhmlibvpyrxmq\n\nganln\nqne\n"},{"insert":[{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}]},{"insert":"dvf"},{"insert":"ac","attributes":{"bold":true}},{"insert":"1302xciwa"},{"insert":"1305rl","attributes":{"bold":true}},{"insert":"08\n"},{"insert":"eyk","attributes":{"bold":true}},{"insert":"y1321apgivydqsjfsehhezukiqtt1307tvjiejlh"},{"insert":"1316zlpkmctoqomgfthbpg","attributes":{"bold":true}},{"insert":"gv"},{"insert":"lb","attributes":{"bold":true}},{"insert":"f\nhntk\njv1uu\n"},{"insert":[{"image":"https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png"}]}] const doc = new Y.Doc() Y.applyUpdate(doc, buffer.fromBase64(oldDoc)) - t.compare(doc.getText('text').getContent().toJSON(), oldVal) + t.compare(doc.getText('text').getContent().toJSON().children, oldVal) } diff --git a/tests/delta.tests.js b/tests/delta.tests.js deleted file mode 100644 index 0e036e76..00000000 --- a/tests/delta.tests.js +++ /dev/null @@ -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} */ (delta.createXmlDelta()) - d.children.insert(['hi']) - d.attributes.set('a', 1) - d.attributes.delete('a', 1) - /** - * @type {Array| 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.Array,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} */ (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) - } - ) - } -} diff --git a/tests/encoding.tests.js b/tests/encoding.tests.js index 0f6db1ad..1121b086 100644 --- a/tests/encoding.tests.js +++ b/tests/encoding.tests.js @@ -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 diff --git a/tests/index.js b/tests/index.js index 148b524b..9bd2894d 100644 --- a/tests/index.js +++ b/tests/index.js @@ -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 () => { diff --git a/tests/y-text.tests.js b/tests/y-text.tests.js index db2db6b0..c78b0494 100644 --- a/tests/y-text.tests.js +++ b/tests/y-text.tests.js @@ -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} - */ - 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) } diff --git a/tests/y-xml.tests.js b/tests/y-xml.tests.js index 9015f9c4..c1a3bffb 100644 --- a/tests/y-xml.tests.js +++ b/tests/y-xml.tests.js @@ -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} */ (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} */ (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') }) } diff --git a/tsconfig.json b/tsconfig.json index a4f93c9c..71beb314 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,5 +19,6 @@ "yjs/testHelper": ["./tests/testHelper.js"] } }, - "include": ["./src/**/*.js", "./tests/**/*.js"] + "include": ["./src/**/*.js", "./tests/**/*.js"], + "exclude": ["../lib0/**"] }