diff --git a/package-lock.json b/package-lock.json index 6cdf6503..b8f1bf46 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ } }, "../lib0": { - "version": "0.2.115", + "version": "0.2.116", "license": "MIT", "bin": { "0ecdsa-generate-keypair": "src/bin/0ecdsa-generate-keypair.js", diff --git a/src/index.js b/src/index.js index 2b6c0a01..2175e1e2 100644 --- a/src/index.js +++ b/src/index.js @@ -3,13 +3,7 @@ export { Doc, Transaction, - YArray as Array, - YMap as Map, - YText as Text, - YXmlText as XmlText, - YXmlHook as XmlHook, - YXmlElement as XmlElement, - YXmlFragment as XmlFragment, + YType as Type, YEvent, Item, AbstractStruct, @@ -46,7 +40,6 @@ export { getItem, getItemCleanStart, getItemCleanEnd, - typeListToArraySnapshot, typeMapGetSnapshot, typeMapGetAllSnapshot, createDocFromSnapshot, @@ -72,7 +65,6 @@ export { equalSnapshots, tryGc, transact, - AbstractConnector, logType, mergeUpdates, mergeUpdatesV2, diff --git a/src/internals.js b/src/internals.js index c7b1f5c1..09cc90ff 100644 --- a/src/internals.js +++ b/src/internals.js @@ -1,4 +1,3 @@ -export * from './utils/AbstractConnector.js' export * from './utils/IdSet.js' export * from './utils/Doc.js' export * from './utils/UpdateDecoder.js' diff --git a/src/structs/ContentFormat.js b/src/structs/ContentFormat.js index 1046d255..e029957a 100644 --- a/src/structs/ContentFormat.js +++ b/src/structs/ContentFormat.js @@ -1,5 +1,5 @@ import { - YText, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, StructStore, Transaction // eslint-disable-line + UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Item, Transaction // eslint-disable-line } from '../internals.js' import * as error from 'lib0/error' @@ -67,7 +67,7 @@ export class ContentFormat { */ integrate (_transaction, item) { // @todo searchmarker are currently unsupported for rich text documents - const p = /** @type {YText} */ (item.parent) + const p = /** @type {import('../ytype.js').YType} */ (item.parent) p._searchMarker = null p._hasFormatting = true } diff --git a/src/structs/ContentType.js b/src/structs/ContentType.js index 25ff3f67..2a2099ab 100644 --- a/src/structs/ContentType.js +++ b/src/structs/ContentType.js @@ -1,33 +1,9 @@ import { - readYArray, - readYMap, - readYText, - readYXmlElement, - readYXmlFragment, - readYXmlHook, - readYXmlText, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Transaction, Item // eslint-disable-line } from '../internals.js' -/** - * @typedef {import('../utils/types.js').YType} YType_CT - */ - import * as error from 'lib0/error' - -/** - * @type {Array<(decoder: UpdateDecoderV1 | UpdateDecoderV2)=>(import('../utils/types.js').YType)>} - * @private - */ -export const typeRefs = [ - readYArray, - readYMap, - readYText, - readYXmlElement, - readYXmlFragment, - readYXmlHook, - readYXmlText -] +import { readYType } from '../ytype.js' export const YArrayRefID = 0 export const YMapRefID = 1 @@ -42,11 +18,11 @@ export const YXmlTextRefID = 6 */ export class ContentType { /** - * @param {YType_CT} type + * @param {import('../ytype.js').YType} type */ constructor (type) { /** - * @type {YType_CT} + * @type {import('../ytype.js').YType} */ this.type = type } @@ -173,4 +149,4 @@ export class ContentType { * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder * @return {ContentType} */ -export const readContentType = decoder => new ContentType(typeRefs[decoder.readTypeRef()](decoder)) +export const readContentType = decoder => new ContentType(readYType(decoder)) diff --git a/src/structs/Item.js b/src/structs/Item.js index b09896d0..f8f69ebf 100644 --- a/src/structs/Item.js +++ b/src/structs/Item.js @@ -29,10 +29,6 @@ import * as error from 'lib0/error' import * as binary from 'lib0/binary' import * as array from 'lib0/array' -/** - * @typedef {import('../utils/types.js').YType} YType__ - */ - /** * @todo This should return several items * @@ -72,7 +68,7 @@ export const followRedone = (store, id) => { export const keepItem = (item, keep) => { while (item !== null && item.keep !== keep) { item.keep = keep - item = /** @type {YType__} */ (item.parent)._item + item = /** @type {YType} */ (item.parent)._item } } @@ -119,7 +115,7 @@ export const splitItem = (transaction, leftItem, diff) => { transaction._mergeStructs.push(rightItem) // update parent._map if (rightItem.parentSub !== null && rightItem.right === null) { - /** @type {YType__} */ (rightItem.parent)._map.set(rightItem.parentSub, rightItem) + /** @type {YType} */ (rightItem.parent)._map.set(rightItem.parentSub, rightItem) } } else { rightItem.left = null @@ -177,7 +173,7 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo if (redone !== null) { return getItemCleanStart(transaction, redone) } - let parentItem = /** @type {YType__} */ (item.parent)._item + let parentItem = /** @type {YType} */ (item.parent)._item /** * @type {Item|null} */ @@ -197,9 +193,9 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo } } /** - * @type {YType__} + * @type {YType} */ - const parentType = /** @type {YType__} */ (parentItem === null ? item.parent : /** @type {ContentType} */ (parentItem.content).type) + const parentType = /** @type {YType} */ (parentItem === null ? item.parent : /** @type {ContentType} */ (parentItem.content).type) if (item.parentSub === null) { // Is an array item. Insert at the old position @@ -212,10 +208,10 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo */ let leftTrace = left // trace redone until parent matches - while (leftTrace !== null && /** @type {YType__} */ (leftTrace.parent)._item !== parentItem) { + while (leftTrace !== null && /** @type {YType} */ (leftTrace.parent)._item !== parentItem) { leftTrace = leftTrace.redone === null ? null : getItemCleanStart(transaction, leftTrace.redone) } - if (leftTrace !== null && /** @type {YType__} */ (leftTrace.parent)._item === parentItem) { + if (leftTrace !== null && /** @type {YType} */ (leftTrace.parent)._item === parentItem) { left = leftTrace break } @@ -227,10 +223,10 @@ export const redoItem = (transaction, item, redoitems, itemsToDelete, ignoreRemo */ let rightTrace = right // trace redone until parent matches - while (rightTrace !== null && /** @type {YType__} */ (rightTrace.parent)._item !== parentItem) { + while (rightTrace !== null && /** @type {YType} */ (rightTrace.parent)._item !== parentItem) { rightTrace = rightTrace.redone === null ? null : getItemCleanStart(transaction, rightTrace.redone) } - if (rightTrace !== null && /** @type {YType__} */ (rightTrace.parent)._item === parentItem) { + if (rightTrace !== null && /** @type {YType} */ (rightTrace.parent)._item === parentItem) { right = rightTrace break } @@ -282,7 +278,7 @@ export class Item extends AbstractStruct { * @param {ID | null} origin * @param {Item | null} right * @param {ID | null} rightOrigin - * @param {YType|ID|null} parent Is a type if integrated, is null if it is possible to copy parent from left or right, is ID before integration to search for it. + * @param {YType|ID|null} parent Is a type if integrated, is null if it is possible to copy parent from left or right, is ID before integration to search for it. * @param {string | null} parentSub * @param {AbstractContent} content */ @@ -309,7 +305,7 @@ export class Item extends AbstractStruct { */ this.rightOrigin = rightOrigin /** - * @type {YType|ID|null} + * @type {YType|ID|null} */ this.parent = parent /** @@ -466,12 +462,12 @@ export class Item extends AbstractStruct { if (left !== null) { o = left.right } else if (this.parentSub !== null) { - o = /** @type {AbstractType} */ (this.parent)._map.get(this.parentSub) || null + o = /** @type {YType} */ (this.parent)._map.get(this.parentSub) || null while (o !== null && o.left !== null) { o = o.left } } else { - o = /** @type {AbstractType} */ (this.parent)._start + o = /** @type {YType} */ (this.parent)._start } // TODO: use something like DeleteSet here (a tree implementation would be best) // @todo use global set definitions @@ -520,13 +516,13 @@ export class Item extends AbstractStruct { } else { let r if (this.parentSub !== null) { - r = /** @type {AbstractType} */ (this.parent)._map.get(this.parentSub) || null + r = /** @type {YType} */ (this.parent)._map.get(this.parentSub) || null while (r !== null && r.left !== null) { r = r.left } } else { - r = /** @type {AbstractType} */ (this.parent)._start - ;/** @type {AbstractType} */ (this.parent)._start = this + r = /** @type {YType} */ (this.parent)._start + ;/** @type {YType} */ (this.parent)._start = this } this.right = r } @@ -534,7 +530,7 @@ export class Item extends AbstractStruct { this.right.left = this } else if (this.parentSub !== null) { // set as current parent value if right === null and this is parentSub - /** @type {AbstractType} */ (this.parent)._map.set(this.parentSub, this) + /** @type {YType} */ (this.parent)._map.set(this.parentSub, this) if (this.left !== null) { // this is the current attribute value of parent. delete right this.left.delete(transaction) @@ -542,14 +538,14 @@ export class Item extends AbstractStruct { } // adjust length of parent if (this.parentSub === null && this.countable && !this.deleted) { - /** @type {AbstractType} */ (this.parent)._length += this.length + /** @type {YType} */ (this.parent)._length += this.length } addStructToIdSet(transaction.insertSet, this) addStruct(transaction.doc.store, this) this.content.integrate(transaction, this) // add parent to transaction.changed - addChangedTypeToTransaction(transaction, /** @type {import('../utils/types.js').YType} */ (this.parent), this.parentSub) - if ((/** @type {AbstractType} */ (this.parent)._item !== null && /** @type {AbstractType} */ (this.parent)._item.deleted) || (this.parentSub !== null && this.right !== null)) { + addChangedTypeToTransaction(transaction, /** @type {YType} */ (this.parent), this.parentSub) + if ((/** @type {YType} */ (this.parent)._item !== null && /** @type {YType} */ (this.parent)._item.deleted) || (this.parentSub !== null && this.right !== null)) { // delete if parent is deleted or if this is not the current attribute value of parent this.delete(transaction) } @@ -609,7 +605,7 @@ export class Item extends AbstractStruct { this.content.constructor === right.content.constructor && this.content.mergeWith(right.content) ) { - const searchMarker = /** @type {AbstractType} */ (this.parent)._searchMarker + const searchMarker = /** @type {YType} */ (this.parent)._searchMarker if (searchMarker) { searchMarker.forEach(marker => { if (marker.p === right) { @@ -642,7 +638,7 @@ export class Item extends AbstractStruct { */ delete (transaction) { if (!this.deleted) { - const parent = /** @type {import('../utils/types.js').YType} */ (this.parent) + const parent = /** @type {YType} */ (this.parent) // adjust the length of parent if (this.countable && this.parentSub === null) { parent._length -= this.length diff --git a/src/types/AbstractType.js b/src/types/AbstractType.js deleted file mode 100644 index 15791ae5..00000000 --- a/src/types/AbstractType.js +++ /dev/null @@ -1,1431 +0,0 @@ -import { - removeEventHandlerListener, - callEventHandlerListeners, - addEventHandlerListener, - createEventHandler, - getState, - isVisible, - ContentType, - createID, - ContentAny, - ContentFormat, - ContentBinary, - ContentJSON, - ContentDeleted, - ContentString, - ContentEmbed, - getItemCleanStart, - noAttributionsManager, - transact, - ItemTextListPosition, - insertText, - deleteText, - ContentDoc, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, createAttributionFromAttributionItems, AbstractAttributionManager, YXmlElement, // eslint-disable-line -} from '../internals.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 - */ -export const warnPrematureAccess = () => { log.warn('Invalid access: Add Yjs type to a document before reading data.') } - -const maxSearchMarker = 80 - -/** - * A unique timestamp that identifies each marker. - * - * Time is relative,.. this is more like an ever-increasing clock. - * - * @type {number} - */ -let globalSearchMarkerTimestamp = 0 - -export class ArraySearchMarker { - /** - * @param {Item} p - * @param {number} index - */ - constructor (p, index) { - p.marker = true - this.p = p - this.index = index - this.timestamp = globalSearchMarkerTimestamp++ - } -} - -/** - * @param {ArraySearchMarker} marker - */ -const refreshMarkerTimestamp = marker => { marker.timestamp = globalSearchMarkerTimestamp++ } - -/** - * This is rather complex so this function is the only thing that should overwrite a marker - * - * @param {ArraySearchMarker} marker - * @param {Item} p - * @param {number} index - */ -const overwriteMarker = (marker, p, index) => { - marker.p.marker = false - marker.p = p - p.marker = true - marker.index = index - marker.timestamp = globalSearchMarkerTimestamp++ -} - -/** - * @param {Array} searchMarker - * @param {Item} p - * @param {number} index - */ -const markPosition = (searchMarker, p, index) => { - if (searchMarker.length >= maxSearchMarker) { - // override oldest marker (we don't want to create more objects) - const marker = searchMarker.reduce((a, b) => a.timestamp < b.timestamp ? a : b) - overwriteMarker(marker, p, index) - return marker - } else { - // create new marker - const pm = new ArraySearchMarker(p, index) - searchMarker.push(pm) - return pm - } -} - -/** - * Search marker help us to find positions in the associative array faster. - * - * They speed up the process of finding a position without much bookkeeping. - * - * A maximum of `maxSearchMarker` objects are created. - * - * This function always returns a refreshed marker (updated timestamp) - * - * @param {import('../utils/types.js').YType} yarray - * @param {number} index - */ -export const findMarker = (yarray, index) => { - if (yarray._start === null || index === 0 || yarray._searchMarker === null) { - return null - } - const marker = yarray._searchMarker.length === 0 ? null : yarray._searchMarker.reduce((a, b) => math.abs(index - a.index) < math.abs(index - b.index) ? a : b) - let p = yarray._start - let pindex = 0 - if (marker !== null) { - p = marker.p - pindex = marker.index - refreshMarkerTimestamp(marker) // we used it, we might need to use it again - } - // iterate to right if possible - while (p.right !== null && pindex < index) { - if (!p.deleted && p.countable) { - if (index < pindex + p.length) { - break - } - pindex += p.length - } - p = p.right - } - // iterate to left if necessary (might be that pindex > index) - while (p.left !== null && pindex > index) { - p = p.left - if (!p.deleted && p.countable) { - pindex -= p.length - } - } - // we want to make sure that p can't be merged with left, because that would screw up everything - // in that cas just return what we have (it is most likely the best marker anyway) - // iterate to left until p can't be merged with left - while (p.left !== null && p.left.id.client === p.id.client && p.left.id.clock + p.left.length === p.id.clock) { - p = p.left - if (!p.deleted && p.countable) { - pindex -= p.length - } - } - - // @todo remove! - // assure position - // { - // let start = yarray._start - // let pos = 0 - // while (start !== p) { - // if (!start.deleted && start.countable) { - // pos += start.length - // } - // start = /** @type {Item} */ (start.right) - // } - // if (pos !== pindex) { - // debugger - // throw new Error('Gotcha position fail!') - // } - // } - // if (marker) { - // if (window.lengths == null) { - // window.lengths = [] - // window.getLengths = () => window.lengths.sort((a, b) => a - b) - // } - // window.lengths.push(marker.index - pindex) - // console.log('distance', marker.index - pindex, 'len', p && p.parent.length) - // } - if (marker !== null && math.abs(marker.index - pindex) < /** @type {any} */ (p.parent).length / maxSearchMarker) { - // adjust existing marker - overwriteMarker(marker, p, pindex) - return marker - } else { - // create new marker - return markPosition(yarray._searchMarker, p, pindex) - } -} - -/** - * Update markers when a change happened. - * - * This should be called before doing a deletion! - * - * @param {Array} searchMarker - * @param {number} index - * @param {number} len If insertion, len is positive. If deletion, len is negative. - */ -export const updateMarkerChanges = (searchMarker, index, len) => { - for (let i = searchMarker.length - 1; i >= 0; i--) { - const m = searchMarker[i] - if (len > 0) { - /** - * @type {Item|null} - */ - let p = m.p - p.marker = false - // Ideally we just want to do a simple position comparison, but this will only work if - // search markers don't point to deleted items for formats. - // Iterate marker to prev undeleted countable position so we know what to do when updating a position - while (p && (p.deleted || !p.countable)) { - p = p.left - if (p && !p.deleted && p.countable) { - // adjust position. the loop should break now - m.index -= p.length - } - } - if (p === null || p.marker === true) { - // remove search marker if updated position is null or if position is already marked - searchMarker.splice(i, 1) - continue - } - m.p = p - p.marker = true - } - if (index < m.index || (len > 0 && index === m.index)) { // a simple index <= m.index check would actually suffice - m.index = math.max(index, m.index + len) - } - } -} - -/** - * Accumulate all (list) children of a type and return them as an Array. - * - * @param {import('../utils/types.js').YType} t - * @return {Array} - */ -export const getTypeChildren = t => { - t.doc ?? warnPrematureAccess() - let s = t._start - const arr = [] - while (s) { - arr.push(s) - s = s.right - } - return arr -} - -/** - * Call event listeners with an event. This will also add an event to all - * parents (for `.observeDeep` handlers). - * - * @param {import('../utils/types.js').YType} type - * @param {Transaction} transaction - * @param {YEvent} event - */ -export const callTypeObservers = (type, transaction, event) => { - const changedType = type - const changedParentTypes = transaction.changedParentTypes - while (true) { - // @ts-ignore - map.setIfUndefined(changedParentTypes, type, () => []).push(event) - if (type._item === null) { - break - } - type = /** @type {import('../utils/types.js').YType} */ (type._item.parent) - } - callEventHandlerListeners(/** @type {any} */ (changedType._eH), event, transaction) -} - -/** - * Abstract Yjs Type class - * @template {delta.DeltaConf} [DConf=any] - */ -export class AbstractType { - constructor () { - /** - * @type {Item|null} - */ - this._item = null - /** - * @type {Map} - */ - this._map = new Map() - /** - * @type {Item|null} - */ - this._start = null - /** - * @type {Doc|null} - */ - this.doc = null - this._length = 0 - /** - * Event handlers - * @type {EventHandler,Transaction>} - */ - this._eH = createEventHandler() - /** - * Deep event handlers - * @type {EventHandler>,Transaction>} - */ - this._dEH = createEventHandler() - /** - * @type {null | Array} - */ - this._searchMarker = null - /** - * @type {delta.DeltaBuilder} - * @private - */ - this._content = /** @type {delta.DeltaBuilderAny} */ (delta.create()) - } - - /** - * Returns a fresh delta that can be used to change this YType. - * @type {delta.DeltaBuilder} - */ - get change () { - return /** @type {any} */ (delta.create()) - } - - /** - * @return {import('../utils/types.js').YType|null} - */ - get parent () { - return /** @type {import('../utils/types.js').YType} */ (this._item ? this._item.parent : null) - } - - /** - * Integrate this type into the Yjs instance. - * - * * Save this struct in the os - * * This type is sent to other client - * * Observer functions are fired - * - * @param {Doc} y The Yjs instance - * @param {Item|null} item - */ - _integrate (y, item) { - this.doc = y - this._item = item - if (this._prelim) { - this.applyDelta(this._prelim) - this._prelim = null - } - } - - /** - * @return {this} - */ - _copy () { - // @ts-ignore - return new this.constructor() - } - - /** - * Makes a copy of this data type that can be included somewhere else. - * - * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. - * - * @return {this} - */ - clone () { - // @todo remove this method from othern types by doing `_copy().apply(this.getContent())` - throw error.methodUnimplemented() - } - - /** - * @param {UpdateEncoderV1 | UpdateEncoderV2} _encoder - */ - _write (_encoder) { } - - /** - * The first non-deleted item - */ - get _first () { - let n = this._start - while (n !== null && n.deleted) { - n = n.right - } - return n - } - - /** - * Creates YEvent and calls all type observers. - * Must be implemented by each type. - * - * @param {Transaction} transaction - * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. - */ - _callObserver (transaction, parentSubs) { - const event = new YEvent(/** @type {any} */ (this), transaction, parentSubs) - callTypeObservers(/** @type {any} */ (this), transaction, event) - if (!transaction.local && this._searchMarker) { - this._searchMarker.length = 0 - } - } - - /** - * Observe all events that are created on this type. - * - * @template {(target: YEvent, tr: Transaction) => void} F - * @param {F} f Observer function - * @return {F} - */ - observe (f) { - addEventHandlerListener(this._eH, f) - return f - } - - /** - * Observe all events that are created by this type and its children. - * - * @template {function(Array>,Transaction):void} F - * @param {F} f Observer function - * @return {F} - */ - observeDeep (f) { - addEventHandlerListener(this._dEH, f) - return f - } - - /** - * Unregister an observer function. - * - * @param {(type:YEvent,tr:Transaction)=>void} f Observer function - */ - unobserve (f) { - removeEventHandlerListener(this._eH, f) - } - - /** - * Unregister an observer function. - * - * @param {function(Array>,Transaction):void} f Observer function - */ - unobserveDeep (f) { - removeEventHandlerListener(this._dEH, f) - } - - /** - * @abstract - * @return {any} - */ - toJSON () {} - - /** - * 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 {boolean} [Deep=false] - * - * @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 {import('../utils/IdSet.js').IdSet?} [opts.deletedItems] - used for computing prevItem in attributes - * @param {Map>|null} [opts.modified] - set of types that should be rendered as modified children - * @param {Deep} [opts.deep] - render child types as delta - * @return {Deep extends true ? delta.Delta> : delta.Delta} The Delta representation of this type. - * - * @public - */ - getContent (am = noAttributionsManager, opts = {}) { - const { itemsToRender = null, retainInserts = false, retainDeletes = false, deletedItems = null, modified = null, deep = false } = opts - const renderAttrs = modified?.get(this) || null - const renderChildren = !!(modified == null || modified.get(this)?.has(null)) - /** - * @type {delta.DeltaBuilderAny} - */ - const d = /** @type {any} */ (delta.create(/** @type {any} */ (this).nodeName || null)) - const optsAll = modified == null ? opts : object.assign({}, opts, { modified: null }) - typeMapGetDelta(d, /** @type {any} */ (this), renderAttrs, am, deep, modified, deletedItems, itemsToRender, opts, optsAll) - 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 if (deep && c.content.constructor === ContentType) { - d.insert([/** @type {any} */(c.content).type.getContent(am, optsAll)], null, attribution) - } else { - d.insert(c.content.getContent(), null, attribution) - } - } else if (renderDelete) { - d.delete(1) - } else if (retainContent) { - if (c.content.constructor === ContentType && modified?.has(/** @type {ContentType} */ (c.content).type)) { - // @todo use current transaction instead - d.modify(/** @type {any} */ (c.content).type.getContent(am, opts)) - } else { - 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.format = object.assign({}, formattingAttribution.format ?? {})) - 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.formatAt = attributedAt - } - if (object.isEmpty(changedAttributedAttributes)) { - d.useAttribution(null) - } else if (attribution != null) { - const attributedAt = (c.deleted ? attribution.deletedAt : attribution.insertedAt) - if (attributedAt != null) formattingAttribution.formatAt = attributedAt - d.useAttribution(formattingAttribution) - } - } - break - } - } - } - } - } - return /** @type {any} */ (d.done(false)) - } - - /** - * Render the difference to another ydoc (which can be empty) and highlight the differences with - * attributions. - * - * @param {AbstractAttributionManager} am - * @return {delta.Delta>} - */ - getContentDeep (am = noAttributionsManager) { - return /** @type {any} */ (this.getContent(am, { deep: true })) - } - - /** - * Apply a {@link Delta} on this shared type. - * - * @param {delta.DeltaAny} d The changes to apply on this element. - * @param {AbstractAttributionManager} am - * - * @public - */ - applyDelta (d, am = noAttributionsManager) { - if (this.doc == null) { - (this._prelim || (this._prelim = /** @type {any} */ (delta.create()))).apply(d) - } else { - // @todo this was moved here from ytext. Make this more generic - transact(this.doc, transaction => { - const currPos = new ItemTextListPosition(null, this._start, 0, new Map(), am) - for (const op of d.children) { - if (delta.$textOp.check(op)) { - insertText(transaction, /** @type {any} */ (this), currPos, op.insert, op.format || {}) - } else if (delta.$insertOp.check(op)) { - for (let i = 0; i < op.insert.length; i++) { - let ins = op.insert[i] - if (delta.$deltaAny.check(ins)) { - if (ins.name != null) { - const t = new YXmlElement(ins.name) - t.applyDelta(ins) - ins = t - } else { - error.unexpectedCase() - } - } - insertText(transaction, /** @type {any} */ (this), currPos, ins, op.format || {}) - } - } else if (delta.$retainOp.check(op)) { - currPos.formatText(transaction, /** @type {any} */ (this), op.retain, op.format || {}) - } else if (delta.$deleteOp.check(op)) { - deleteText(transaction, currPos, op.delete) - } else if (delta.$modifyOp.check(op)) { - if (currPos.right) { - /** @type {ContentType} */ (currPos.right.content).type.applyDelta(op.value) - } else { - error.unexpectedCase() - } - currPos.formatText(transaction, /** @type {any} */ (this), 1, op.format || {}) - } else { - error.unexpectedCase() - } - } - for (const op of d.attrs) { - if (delta.$setAttrOp.check(op)) { - typeMapSet(transaction, /** @type {any} */ (this), /** @type {any} */ (op.key), op.value) - } else if (delta.$deleteOp.check(op)) { - typeMapDelete(transaction, /** @type {any} */ (this), /** @type {any} */ (op.key)) - } else { - const sub = typeMapGet(/** @type {any} */ (this), /** @type {any} */ (op.key)) - if (!(sub instanceof AbstractType)) { - error.unexpectedCase() - } - sub.applyDelta(op.value) - } - } - }) - } - } -} - -/** - * @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.DeltaConf} DConf - * @typedef {delta.DeltaConfOverwrite]: delta.DeltaConfGetAttrs[K] }, - * children: (Extract,AbstractType> extends AbstractType ? ( - * unknown extends SubDConf ? never : (delta.Delta>) - * ) : never) | Exclude,AbstractType> - * }> - * } DeltaConfTypesToDelta - */ - -/** - * @param {AbstractType} type - * @param {number} start - * @param {number} end - * @return {Array} - * - * @private - * @function - */ -export const typeListSlice = (type, start, end) => { - type.doc ?? warnPrematureAccess() - if (start < 0) { - start = type._length + start - } - if (end < 0) { - end = type._length + end - } - let len = end - start - const cs = [] - let n = type._start - while (n !== null && len > 0) { - if (n.countable && !n.deleted) { - const c = n.content.getContent() - if (c.length <= start) { - start -= c.length - } else { - for (let i = start; i < c.length && len > 0; i++) { - cs.push(c[i]) - len-- - } - start = 0 - } - } - n = n.right - } - return cs -} - -/** - * @param {import('../utils/types.js').YType} type - * @return {Array} - * - * @private - * @function - */ -export const typeListToArray = type => { - type.doc ?? warnPrematureAccess() - const cs = [] - let n = type._start - while (n !== null) { - if (n.countable && !n.deleted) { - const c = n.content.getContent() - for (let i = 0; i < c.length; i++) { - cs.push(c[i]) - } - } - n = n.right - } - return cs -} - -/** - * @param {AbstractType} type - * @param {Snapshot} snapshot - * @return {Array} - * - * @private - * @function - */ -export const typeListToArraySnapshot = (type, snapshot) => { - const cs = [] - let n = type._start - while (n !== null) { - if (n.countable && isVisible(n, snapshot)) { - const c = n.content.getContent() - for (let i = 0; i < c.length; i++) { - cs.push(c[i]) - } - } - n = n.right - } - return cs -} - -/** - * Executes a provided function on once on every element of this YArray. - * - * @param {AbstractType} type - * @param {function(any,number,any):void} f A function to execute on every element of this YArray. - * - * @private - * @function - */ -export const typeListForEach = (type, f) => { - let index = 0 - let n = type._start - type.doc ?? warnPrematureAccess() - while (n !== null) { - if (n.countable && !n.deleted) { - const c = n.content.getContent() - for (let i = 0; i < c.length; i++) { - f(c[i], index++, type) - } - } - n = n.right - } -} - -/** - * @template C,R - * @param {AbstractType} type - * @param {function(C,number,AbstractType):R} f - * @return {Array} - * - * @private - * @function - */ -export const typeListMap = (type, f) => { - /** - * @type {Array} - */ - const result = [] - typeListForEach(type, (c, i) => { - result.push(f(c, i, type)) - }) - return result -} - -/** - * @param {AbstractType} type - * @return {IterableIterator} - * - * @private - * @function - */ -export const typeListCreateIterator = type => { - let n = type._start - /** - * @type {Array|null} - */ - let currentContent = null - let currentContentIndex = 0 - return { - [Symbol.iterator] () { - return this - }, - next: () => { - // find some content - if (currentContent === null) { - while (n !== null && n.deleted) { - n = n.right - } - // check if we reached the end, no need to check currentContent, because it does not exist - if (n === null) { - return { - done: true, - value: undefined - } - } - // we found n, so we can set currentContent - currentContent = n.content.getContent() - currentContentIndex = 0 - n = n.right // we used the content of n, now iterate to next - } - const value = currentContent[currentContentIndex++] - // check if we need to empty currentContent - if (currentContent.length <= currentContentIndex) { - currentContent = null - } - return { - done: false, - value - } - } - } -} - -/** - * 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 {Snapshot} snapshot - * - * @private - * @function - */ -export const typeListForEachSnapshot = (type, f, snapshot) => { - let index = 0 - let n = type._start - while (n !== null) { - if (n.countable && isVisible(n, snapshot)) { - const c = n.content.getContent() - for (let i = 0; i < c.length; i++) { - f(c[i], index++, type) - } - } - n = n.right - } -} - -/** - * @param {import('../utils/types.js').YType} type - * @param {number} index - * @return {any} - * - * @private - * @function - */ -export const typeListGet = (type, index) => { - type.doc ?? warnPrematureAccess() - const marker = findMarker(type, index) - let n = type._start - if (marker !== null) { - n = marker.p - index -= marker.index - } - for (; n !== null; n = n.right) { - if (!n.deleted && n.countable) { - if (index < n.length) { - return n.content.getContent()[index] - } - index -= n.length - } - } -} - -/** - * @param {Transaction} transaction - * @param {YType_} parent - * @param {Item?} referenceItem - * @param {Array<_YValue>} content - * - * @private - * @function - */ -export const typeListInsertGenericsAfter = (transaction, parent, referenceItem, content) => { - let left = referenceItem - const doc = transaction.doc - const ownClientId = doc.clientID - const store = doc.store - const right = referenceItem === null ? parent._start : referenceItem.right - /** - * @type {Array|number|null>} - */ - let jsonContent = [] - const packJsonContent = () => { - if (jsonContent.length > 0) { - left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentAny(jsonContent)) - left.integrate(transaction, 0) - jsonContent = [] - } - } - content.forEach(c => { - if (c === null) { - jsonContent.push(c) - } else { - switch (c.constructor) { - case Number: - case Object: - case undefined: - case Boolean: - case Array: - case String: - case BigInt: - case Date: - jsonContent.push(c) - break - default: - packJsonContent() - switch (c.constructor) { - case Uint8Array: - case ArrayBuffer: - left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentBinary(new Uint8Array(/** @type {Uint8Array} */ (c)))) - left.integrate(transaction, 0) - break - case Doc: - left = new Item(createID(ownClientId, getState(store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentDoc(/** @type {Doc} */ (c))) - left.integrate(transaction, 0) - 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(/** @type {any} */ (c))) - left.integrate(transaction, 0) - } else { - throw new Error('Unexpected content type in insert operation') - } - } - } - } - }) - packJsonContent() -} - -const lengthExceeded = () => error.create('Length exceeded!') - -/** - * @param {Transaction} transaction - * @param {YType_} parent - * @param {number} index - * @param {Array|Array|number|null|string|Uint8Array>} content - * - * @private - * @function - */ -export const typeListInsertGenerics = (transaction, parent, index, content) => { - if (index > parent._length) { - throw lengthExceeded() - } - if (index === 0) { - if (parent._searchMarker) { - updateMarkerChanges(parent._searchMarker, index, content.length) - } - return typeListInsertGenericsAfter(transaction, parent, null, content) - } - const startIndex = index - const marker = findMarker(parent, index) - let n = parent._start - if (marker !== null) { - n = marker.p - index -= marker.index - // we need to iterate one to the left so that the algorithm works - if (index === 0) { - // @todo refactor this as it actually doesn't consider formats - n = n.prev // important! get the left undeleted item so that we can actually decrease index - index += (n && n.countable && !n.deleted) ? n.length : 0 - } - } - for (; n !== null; n = n.right) { - if (!n.deleted && n.countable) { - if (index <= n.length) { - if (index < n.length) { - // insert in-between - getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index)) - } - break - } - index -= n.length - } - } - if (parent._searchMarker) { - updateMarkerChanges(parent._searchMarker, startIndex, content.length) - } - return typeListInsertGenericsAfter(transaction, parent, n, content) -} - -/** - * Pushing content is special as we generally want to push after the last item. So we don't have to update - * the search marker. - * - * @param {Transaction} transaction - * @param {YType_} parent - * @param {Array|Array|number|null|string|Uint8Array>} content - * - * @private - * @function - */ -export const typeListPushGenerics = (transaction, parent, content) => { - // Use the marker with the highest index and iterate to the right. - const marker = (parent._searchMarker || []).reduce((maxMarker, currMarker) => currMarker.index > maxMarker.index ? currMarker : maxMarker, { index: 0, p: parent._start }) - let n = marker.p - if (n) { - while (n.right) { - n = n.right - } - } - return typeListInsertGenericsAfter(transaction, parent, n, content) -} - -/** - * @param {Transaction} transaction - * @param {import('../utils/types.js').YType} parent - * @param {number} index - * @param {number} length - * - * @private - * @function - */ -export const typeListDelete = (transaction, parent, index, length) => { - if (length === 0) { return } - const startIndex = index - const startLength = length - const marker = findMarker(parent, index) - let n = parent._start - if (marker !== null) { - n = marker.p - index -= marker.index - } - // compute the first item to be deleted - for (; n !== null && index > 0; n = n.right) { - if (!n.deleted && n.countable) { - if (index < n.length) { - getItemCleanStart(transaction, createID(n.id.client, n.id.clock + index)) - } - index -= n.length - } - } - // delete all items until done - while (length > 0 && n !== null) { - if (!n.deleted) { - if (length < n.length) { - getItemCleanStart(transaction, createID(n.id.client, n.id.clock + length)) - } - n.delete(transaction) - length -= n.length - } - n = n.right - } - if (length > 0) { - throw lengthExceeded() - } - if (parent._searchMarker) { - updateMarkerChanges(parent._searchMarker, startIndex, -startLength + length /* in case we remove the above exception */) - } -} - -/** - * @param {Transaction} transaction - * @param {YType_} parent - * @param {string} key - * - * @private - * @function - */ -export const typeMapDelete = (transaction, parent, key) => { - const c = parent._map.get(key) - if (c !== undefined) { - c.delete(transaction) - } -} - -/** - * @param {Transaction} transaction - * @param {AbstractType} parent - * @param {string} key - * @param {_YValue} value - * - * @private - * @function - */ -export const typeMapSet = (transaction, parent, key, value) => { - const left = parent._map.get(key) || null - const doc = transaction.doc - const ownClientId = doc.clientID - let content - if (value == null) { - content = new ContentAny([value]) - } else { - switch (value.constructor) { - case Number: - case Object: - case Boolean: - case Array: - case String: - case Date: - case BigInt: - content = new ContentAny([value]) - break - case Uint8Array: - content = new ContentBinary(/** @type {Uint8Array} */ (value)) - break - case Doc: - content = new ContentDoc(/** @type {Doc} */ (value)) - break - default: - if (value instanceof AbstractType) { - content = new ContentType(/** @type {any} */ (value)) - } else { - throw new Error('Unexpected content type') - } - } - } - new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, null, null, parent, key, content).integrate(transaction, 0) -} - -/** - * @param {AbstractType} parent - * @param {string} key - * @return {Object|number|null|Array|string|Uint8Array|AbstractType|undefined} - * - * @private - * @function - */ -export const typeMapGet = (parent, key) => { - parent.doc ?? warnPrematureAccess() - const val = parent._map.get(key) - return val !== undefined && !val.deleted ? val.content.getContent()[val.length - 1] : undefined -} - -/** - * @param {AbstractType} parent - * @return {Object|number|null|Array|string|Uint8Array|AbstractType|undefined>} - * - * @private - * @function - */ -export const typeMapGetAll = (parent) => { - /** - * @type {Object} - */ - const res = {} - parent.doc ?? warnPrematureAccess() - parent._map.forEach((value, key) => { - if (!value.deleted) { - res[key] = value.content.getContent()[value.length - 1] - } - }) - return res -} - -/** - * @todo move this to getContent/getDelta - * - * 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.DeltaBuilderAny} TypeDelta - * @param {TypeDelta} d - * @param {YType_} parent - * @param {Set?} attrsToRender - * @param {import('../internals.js').AbstractAttributionManager} am - * @param {boolean} deep - * @param {Set|Map|null} [modified] - set of types that should be rendered as modified children - * @param {import('../utils/IdSet.js').IdSet?} [deletedItems] - * @param {import('../utils/IdSet.js').IdSet?} [itemsToRender] - * @param {any} [opts] - * @param {any} [optsAll] - * - * @private - * @function - */ -export const typeMapGetDelta = (d, parent, attrsToRender, am, deep, modified, deletedItems, itemsToRender, opts, optsAll) => { - // @todo support modified ops! - /** - * @param {Item} item - * @param {string} key - */ - const renderAttrs = (item, key) => { - /** - * @type {Array>} - */ - const cs = [] - am.readContent(cs, item.id.client, item.id.clock, item.deleted, item.content, 1) - const { deleted, attrs, content } = cs[cs.length - 1] - const attribution = createAttributionFromAttributionItems(attrs, deleted) - let c = array.last(content.getContent()) - if (deleted) { - if (itemsToRender == null || itemsToRender.hasId(item.lastId)) { - d.deleteAttr(key, attribution, c) - } - } else if (deep && c instanceof AbstractType && modified?.has(c)) { - d.modifyAttr(key, c.getContent(am, opts)) - } else { - // find prev content - let prevContentItem = item - // this algorithm is problematic. should check all previous content using am.readcontent - for (; prevContentItem.left !== null && deletedItems?.hasId(prevContentItem.left.lastId); prevContentItem = prevContentItem.left) { - // nop - } - const prevValue = (prevContentItem !== item && itemsToRender?.hasId(prevContentItem.lastId)) ? array.last(prevContentItem.content.getContent()) : undefined - if (deep && c instanceof AbstractType) { - c = /** @type {any} */(c).getContent(am, optsAll) - } - d.setAttr(key, c, attribution, prevValue) - } - } - if (attrsToRender == null) { - parent._map.forEach(renderAttrs) - } else { - attrsToRender.forEach(key => key != null && renderAttrs(/** @type {Item} */ (parent._map.get(key)), key)) - } -} - -/** - * @param {AbstractType} parent - * @param {string} key - * @return {boolean} - * - * @private - * @function - */ -export const typeMapHas = (parent, key) => { - parent.doc ?? warnPrematureAccess() - const val = parent._map.get(key) - return val !== undefined && !val.deleted -} - -/** - * @param {AbstractType} parent - * @param {string} key - * @param {Snapshot} snapshot - * @return {Object|number|null|Array|string|Uint8Array|AbstractType|undefined} - * - * @private - * @function - */ -export const typeMapGetSnapshot = (parent, key, snapshot) => { - let v = parent._map.get(key) || null - while (v !== null && (!snapshot.sv.has(v.id.client) || v.id.clock >= (snapshot.sv.get(v.id.client) || 0))) { - v = v.left - } - return v !== null && isVisible(v, snapshot) ? v.content.getContent()[v.length - 1] : undefined -} - -/** - * @param {AbstractType} parent - * @param {Snapshot} snapshot - * @return {Object|number|null|Array|string|Uint8Array|AbstractType|undefined>} - * - * @private - * @function - */ -export const typeMapGetAllSnapshot = (parent, snapshot) => { - /** - * @type {Object} - */ - const res = {} - parent._map.forEach((value, key) => { - /** - * @type {Item|null} - */ - let v = value - while (v !== null && (!snapshot.sv.has(v.id.client) || v.id.clock >= (snapshot.sv.get(v.id.client) || 0))) { - v = v.left - } - if (v !== null && isVisible(v, snapshot)) { - res[key] = v.content.getContent()[v.length - 1] - } - }) - return res -} - -/** - * @param {AbstractType & { _map: Map }} type - * @return {IterableIterator>} - * - * @private - * @function - */ -export const createMapIterator = type => { - type.doc ?? warnPrematureAccess() - return iterator.iteratorFilter(type._map.entries(), /** @param {any} entry */ entry => !entry[1].deleted) -} diff --git a/src/types/Type.js b/src/types/Type.js deleted file mode 100644 index 320a8bb7..00000000 --- a/src/types/Type.js +++ /dev/null @@ -1,270 +0,0 @@ -/** - * @module YArray - */ - -import { - AbstractType, - typeListGet, - typeListToArray, - typeListForEach, - typeListCreateIterator, - typeListInsertGenerics, - typeListPushGenerics, - typeListDelete, - typeListMap, - YArrayRefID, - transact, - warnPrematureAccess, - typeListSlice, - noAttributionsManager, - AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line -} from '../internals.js' - -import * as delta from 'lib0/delta' - -/** - * A shared Array implementation. - * @template {import('../utils/types.js').YValue} T - * @extends {AbstractType,YArray>} - * @implements {Iterable} - */ -// @todo remove this -// @ts-ignore -export class YType extends AbstractType { - constructor () { - super() - /** - * @type {Array?} - * @private - */ - this._prelimContent = [] - /** - * @type {Array} - */ - this._searchMarker = [] - } - - /** - * Construct a new YArray containing the specified items. - * @template {import('../utils/types.js').YValue} T - * @param {Array} items - * @return {YArray} - */ - static from (items) { - /** - * @type {YArray} - */ - const a = new YArray() - a.push(items) - return a - } - - /** - * Integrate this type into the Yjs instance. - * - * * Save this struct in the os - * * This type is sent to other client - * * Observer functions are fired - * - * @param {Doc} y The Yjs instance - * @param {Item?} item - */ - _integrate (y, item) { - super._integrate(y, item) - this.insert(0, /** @type {Array} */ (this._prelimContent)) - this._prelimContent = null - } - - /** - * Makes a copy of this data type that can be included somewhere else. - * - * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. - * - * @return {YArray} - */ - clone () { - /** - * @type {this} - */ - const arr = /** @type {this} */ (new YArray()) - arr.insert(0, this.toArray().map(el => - // @ts-ignore - el instanceof AbstractType ? /** @type {any} */ (el.clone()) : el - )) - return arr - } - - get length () { - this.doc ?? warnPrematureAccess() - return this._length - } - - /** - * Inserts new content at an index. - * - * Important: This function expects an array of content. Not just a content - * object. The reason for this "weirdness" is that inserting several elements - * is very efficient when it is done as a single operation. - * - * @example - * // Insert character 'a' at position 0 - * yarray.insert(0, ['a']) - * // Insert numbers 1, 2 at position 1 - * yarray.insert(1, [1, 2]) - * - * @param {number} index The index to insert content at. - * @param {Array} content The array of content - */ - insert (index, content) { - if (this.doc !== null) { - transact(this.doc, transaction => { - typeListInsertGenerics(transaction, this, index, /** @type {any} */ (content)) - }) - } else { - /** @type {Array} */ (this._prelimContent).splice(index, 0, ...content) - } - } - - /** - * Appends content to this YArray. - * - * @param {Array} content Array of content to append. - * - * @todo Use the following implementation in all types. - */ - push (content) { - if (this.doc !== null) { - transact(this.doc, transaction => { - typeListPushGenerics(transaction, this, /** @type {any} */ (content)) - }) - } else { - /** @type {Array} */ (this._prelimContent).push(...content) - } - } - - /** - * Prepends content to this YArray. - * - * @param {Array} content Array of content to prepend. - */ - unshift (content) { - this.insert(0, content) - } - - /** - * Deletes elements starting from an index. - * - * @param {number} index Index at which to start deleting elements - * @param {number} length The number of elements to remove. Defaults to 1. - */ - delete (index, length = 1) { - if (this.doc !== null) { - transact(this.doc, transaction => { - typeListDelete(transaction, this, index, length) - }) - } else { - /** @type {Array} */ (this._prelimContent).splice(index, length) - } - } - - /** - * Returns the i-th element from a YArray. - * - * @param {number} index The index of the element to return from the YArray - * @return {T} - */ - get (index) { - return typeListGet(this, index) - } - - /** - * Transforms this YArray to a JavaScript Array. - * - * @return {Array} - */ - toArray () { - return typeListToArray(this) - } - - /** - * Render the difference to another ydoc (which can be empty) and highlight the differences with - * attributions. - * - * Note that deleted content that was not deleted in prevYdoc is rendered as an insertion with the - * attribution `{ isDeleted: true, .. }`. - * - * @param {AbstractAttributionManager} am - * @return {delta.ArrayDelta>} The Delta representation of this type. - * - * @public - */ - getContentDeep (am = noAttributionsManager) { - return super.getContentDeep(am) - } - - /** - * Returns a portion of this YArray into a JavaScript Array selected - * from start to end (end not included). - * - * @param {number} [start] - * @param {number} [end] - * @return {Array} - */ - slice (start = 0, end = this.length) { - return typeListSlice(this, start, end) - } - - /** - * Transforms this Shared Type to a JSON object. - * - * @return {Array} - */ - toJSON () { - return this.map(c => c instanceof AbstractType ? c.toJSON() : c) - } - - /** - * Returns an Array with the result of calling a provided function on every - * element of this YArray. - * - * @template M - * @param {function(T,number,YArray):M} f Function that produces an element of the new Array - * @return {Array} A new array with each element being the result of the - * callback function - */ - map (f) { - return typeListMap(this, /** @type {any} */ (f)) - } - - /** - * Executes a provided function once on every element of this YArray. - * - * @param {function(T,number,YArray):void} f A function to execute on every element of this YArray. - */ - forEach (f) { - typeListForEach(this, f) - } - - /** - * @return {IterableIterator} - */ - [Symbol.iterator] () { - return typeListCreateIterator(this) - } - - /** - * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder - */ - _write (encoder) { - encoder.writeTypeRef(YArrayRefID) - } -} - -/** - * @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder - * @return {import('../utils/types.js').YType} - * - * @private - * @function - */ -export const readYArray = _decoder => new YArray() diff --git a/src/types/YArray.js b/src/types/YArray.js deleted file mode 100644 index eaad7db1..00000000 --- a/src/types/YArray.js +++ /dev/null @@ -1,270 +0,0 @@ -/** - * @module YArray - */ - -import { - AbstractType, - typeListGet, - typeListToArray, - typeListForEach, - typeListCreateIterator, - typeListInsertGenerics, - typeListPushGenerics, - typeListDelete, - typeListMap, - YArrayRefID, - transact, - warnPrematureAccess, - typeListSlice, - noAttributionsManager, - AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line -} from '../internals.js' - -import * as delta from 'lib0/delta' // eslint-disable-line - -/** - * A shared Array implementation. - * @template {import('../utils/types.js').YValue} T - * @extends {AbstractType,YArray>} - * @implements {Iterable} - */ -// @todo remove this -// @ts-ignore -export class YArray extends AbstractType { - constructor () { - super() - /** - * @type {Array?} - * @private - */ - this._prelimContent = [] - /** - * @type {Array} - */ - this._searchMarker = [] - } - - /** - * Construct a new YArray containing the specified items. - * @template {import('../utils/types.js').YValue} T - * @param {Array} items - * @return {YArray} - */ - static from (items) { - /** - * @type {YArray} - */ - const a = new YArray() - a.push(items) - return a - } - - /** - * Integrate this type into the Yjs instance. - * - * * Save this struct in the os - * * This type is sent to other client - * * Observer functions are fired - * - * @param {Doc} y The Yjs instance - * @param {Item?} item - */ - _integrate (y, item) { - super._integrate(y, item) - this.insert(0, /** @type {Array} */ (this._prelimContent)) - this._prelimContent = null - } - - /** - * Makes a copy of this data type that can be included somewhere else. - * - * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. - * - * @return {YArray} - */ - clone () { - /** - * @type {this} - */ - const arr = /** @type {this} */ (new YArray()) - arr.insert(0, this.toArray().map(el => - // @ts-ignore - el instanceof AbstractType ? /** @type {any} */ (el.clone()) : el - )) - return arr - } - - get length () { - this.doc ?? warnPrematureAccess() - return this._length - } - - /** - * Inserts new content at an index. - * - * Important: This function expects an array of content. Not just a content - * object. The reason for this "weirdness" is that inserting several elements - * is very efficient when it is done as a single operation. - * - * @example - * // Insert character 'a' at position 0 - * yarray.insert(0, ['a']) - * // Insert numbers 1, 2 at position 1 - * yarray.insert(1, [1, 2]) - * - * @param {number} index The index to insert content at. - * @param {Array} content The array of content - */ - insert (index, content) { - if (this.doc !== null) { - transact(this.doc, transaction => { - typeListInsertGenerics(transaction, this, index, /** @type {any} */ (content)) - }) - } else { - /** @type {Array} */ (this._prelimContent).splice(index, 0, ...content) - } - } - - /** - * Appends content to this YArray. - * - * @param {Array} content Array of content to append. - * - * @todo Use the following implementation in all types. - */ - push (content) { - if (this.doc !== null) { - transact(this.doc, transaction => { - typeListPushGenerics(transaction, this, /** @type {any} */ (content)) - }) - } else { - /** @type {Array} */ (this._prelimContent).push(...content) - } - } - - /** - * Prepends content to this YArray. - * - * @param {Array} content Array of content to prepend. - */ - unshift (content) { - this.insert(0, content) - } - - /** - * Deletes elements starting from an index. - * - * @param {number} index Index at which to start deleting elements - * @param {number} length The number of elements to remove. Defaults to 1. - */ - delete (index, length = 1) { - if (this.doc !== null) { - transact(this.doc, transaction => { - typeListDelete(transaction, this, index, length) - }) - } else { - /** @type {Array} */ (this._prelimContent).splice(index, length) - } - } - - /** - * Returns the i-th element from a YArray. - * - * @param {number} index The index of the element to return from the YArray - * @return {T} - */ - get (index) { - return typeListGet(this, index) - } - - /** - * Transforms this YArray to a JavaScript Array. - * - * @return {Array} - */ - toArray () { - return typeListToArray(this) - } - - /** - * Render the difference to another ydoc (which can be empty) and highlight the differences with - * attributions. - * - * Note that deleted content that was not deleted in prevYdoc is rendered as an insertion with the - * attribution `{ isDeleted: true, .. }`. - * - * @param {AbstractAttributionManager} am - * @return {delta.ArrayDelta>} The Delta representation of this type. - * - * @public - */ - getContentDeep (am = noAttributionsManager) { - return super.getContentDeep(am) - } - - /** - * Returns a portion of this YArray into a JavaScript Array selected - * from start to end (end not included). - * - * @param {number} [start] - * @param {number} [end] - * @return {Array} - */ - slice (start = 0, end = this.length) { - return typeListSlice(this, start, end) - } - - /** - * Transforms this Shared Type to a JSON object. - * - * @return {Array} - */ - toJSON () { - return this.map(c => c instanceof AbstractType ? c.toJSON() : c) - } - - /** - * Returns an Array with the result of calling a provided function on every - * element of this YArray. - * - * @template M - * @param {function(T,number,YArray):M} f Function that produces an element of the new Array - * @return {Array} A new array with each element being the result of the - * callback function - */ - map (f) { - return typeListMap(this, /** @type {any} */ (f)) - } - - /** - * Executes a provided function once on every element of this YArray. - * - * @param {function(T,number,YArray):void} f A function to execute on every element of this YArray. - */ - forEach (f) { - typeListForEach(this, f) - } - - /** - * @return {IterableIterator} - */ - [Symbol.iterator] () { - return typeListCreateIterator(this) - } - - /** - * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder - */ - _write (encoder) { - encoder.writeTypeRef(YArrayRefID) - } -} - -/** - * @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder - * @return {import('../utils/types.js').YType} - * - * @private - * @function - */ -export const readYArray = _decoder => new YArray() diff --git a/src/types/YMap.js b/src/types/YMap.js deleted file mode 100644 index b610c1df..00000000 --- a/src/types/YMap.js +++ /dev/null @@ -1,244 +0,0 @@ -/** - * @module YMap - */ - -import { - AbstractType, - typeMapDelete, - typeMapSet, - typeMapGet, - typeMapHas, - createMapIterator, - YMapRefID, - transact, - warnPrematureAccess, - UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item // eslint-disable-line -} from '../internals.js' - -import * as iterator from 'lib0/iterator' -import * as delta from 'lib0/delta' // eslint-disable-line - -/** - * @template MapType - * A shared Map implementation. - * - * @extends AbstractType> - * @implements {Iterable<[string, MapType]>} - */ -export class YMap extends AbstractType { - /** - * - * @param {Iterable=} entries - an optional iterable to initialize the YMap - */ - constructor (entries) { - super() - /** - * @type {Map?} - * @private - */ - this._prelimContent = null - - if (entries === undefined) { - this._prelimContent = new Map() - } else { - this._prelimContent = new Map(entries) - } - } - - /** - * Integrate this type into the Yjs instance. - * - * * Save this struct in the os - * * This type is sent to other client - * * Observer functions are fired - * - * @param {Doc} y The Yjs instance - * @param {Item?} item - */ - _integrate (y, item) { - super._integrate(y, item) - ;/** @type {Map} */ (this._prelimContent).forEach((value, key) => { - this.set(key, value) - }) - this._prelimContent = null - } - - /** - * Makes a copy of this data type that can be included somewhere else. - * - * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. - * - * @return {this} - */ - clone () { - const map = this._copy() - this.forEach((value, key) => { - map.set(key, value instanceof AbstractType ? /** @type {typeof value} */ (value.clone()) : value) - }) - return map - } - - /** - * Transforms this Shared Type to a JSON object. - * - * @return {Object} - */ - toJSON () { - this.doc ?? warnPrematureAccess() - /** - * @type {Object} - */ - const map = {} - this._map.forEach((item, key) => { - if (!item.deleted) { - const v = item.content.getContent()[item.length - 1] - map[key] = v instanceof AbstractType ? v.toJSON() : v - } - }) - return map - } - - /** - * Returns the size of the YMap (count of key/value pairs) - * - * @return {number} - */ - get size () { - return [...createMapIterator(this)].length - } - - /** - * Returns the keys for each element in the YMap Type. - * - * @return {IterableIterator} - */ - keys () { - return iterator.iteratorMap(createMapIterator(this), /** @param {any} v */ v => v[0]) - } - - /** - * Returns the values for each element in the YMap Type. - * - * @return {IterableIterator} - */ - values () { - return iterator.iteratorMap(createMapIterator(this), /** @param {any} v */ v => v[1].content.getContent()[v[1].length - 1]) - } - - /** - * Returns an Iterator of [key, value] pairs - * - * @return {IterableIterator<[string, MapType]>} - */ - entries () { - return iterator.iteratorMap(createMapIterator(this), /** @param {any} v */ v => /** @type {any} */ ([v[0], v[1].content.getContent()[v[1].length - 1]])) - } - - /** - * Executes a provided function on once on every key-value pair. - * - * @param {function(MapType,string,YMap):void} f A function to execute on every element of this YArray. - */ - forEach (f) { - this.doc ?? warnPrematureAccess() - this._map.forEach((item, key) => { - if (!item.deleted) { - f(item.content.getContent()[item.length - 1], key, this) - } - }) - } - - /** - * Returns an Iterator of [key, value] pairs - * - * @return {IterableIterator<[string, MapType]>} - */ - [Symbol.iterator] () { - return this.entries() - } - - /** - * Remove a specified element from this YMap. - * - * @param {string} key The key of the element to remove. - */ - delete (key) { - if (this.doc !== null) { - transact(this.doc, transaction => { - typeMapDelete(transaction, this, key) - }) - } else { - /** @type {Map} */ (this._prelimContent).delete(key) - } - } - - /** - * Adds or updates an element with a specified key and value. - * @template {MapType} VAL - * - * @param {string} key The key of the element to add to this YMap - * @param {VAL} value The value of the element to add - * @return {VAL} - */ - set (key, value) { - if (this.doc !== null) { - transact(this.doc, transaction => { - typeMapSet(transaction, this, key, /** @type {any} */ (value)) - }) - } else { - /** @type {Map} */ (this._prelimContent).set(key, value) - } - return value - } - - /** - * Returns a specified element from this YMap. - * - * @param {string} key - * @return {MapType|undefined} - */ - get (key) { - return /** @type {any} */ (typeMapGet(this, key)) - } - - /** - * Returns a boolean indicating whether the specified key exists or not. - * - * @param {string} key The key to test. - * @return {boolean} - */ - has (key) { - return typeMapHas(this, key) - } - - /** - * Removes all elements from this YMap. - */ - clear () { - if (this.doc !== null) { - transact(this.doc, transaction => { - this.forEach(function (_value, key, map) { - typeMapDelete(transaction, map, key) - }) - }) - } else { - /** @type {Map} */ (this._prelimContent).clear() - } - } - - /** - * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder - */ - _write (encoder) { - encoder.writeTypeRef(YMapRefID) - } -} - -/** - * @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder - * @return {import('../utils/types.js').YType} - * - * @private - * @function - */ -export const readYMap = _decoder => new YMap() diff --git a/src/types/YText.js b/src/types/YText.js deleted file mode 100644 index 3a45cd87..00000000 --- a/src/types/YText.js +++ /dev/null @@ -1,934 +0,0 @@ -/** - * @module YText - */ - -import { - AbstractType, - getItemCleanStart, - getState, - createID, - YTextRefID, - transact, - ContentEmbed, - GC, - ContentFormat, - ContentString, - iterateStructsByIdSet, - findMarker, - typeMapDelete, - typeMapSet, - typeMapGet, - typeMapGetAll, - updateMarkerChanges, - ContentType, - warnPrematureAccess, - noAttributionsManager, AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item, Transaction, // eslint-disable-line - createIdSet, - equalAttrs -} from '../internals.js' - -import * as math from 'lib0/math' -import * as traits from 'lib0/traits' -import * as map from 'lib0/map' -import * as error from 'lib0/error' - -export class ItemTextListPosition { - /** - * @param {Item|null} left - * @param {Item|null} right - * @param {number} index - * @param {Map} currentAttributes - * @param {AbstractAttributionManager} am - */ - constructor (left, right, index, currentAttributes, am) { - this.left = left - this.right = right - this.index = index - this.currentAttributes = currentAttributes - this.am = am - } - - /** - * Only call this if you know that this.right is defined - */ - forward () { - if (this.right === null) { - error.unexpectedCase() - } - switch (this.right.content.constructor) { - case ContentFormat: - if (!this.right.deleted) { - updateCurrentAttributes(this.currentAttributes, /** @type {ContentFormat} */ (this.right.content)) - } - break - default: - this.index += this.am.contentLength(this.right) - break - } - this.left = this.right - this.right = this.right.right - } - - /** - * @param {Transaction} transaction - * @param {import('../utils/types.js').YType} parent - * @param {number} length - * @param {Object} attributes - * - * @function - */ - formatText (transaction, parent, length, attributes) { - const doc = transaction.doc - const ownClientId = doc.clientID - minimizeAttributeChanges(this, attributes) - const negatedAttributes = insertAttributes(transaction, parent, this, attributes) - // iterate until first non-format or null is found - // delete all formats with attributes[format.key] != null - // also check the attributes after the first non-format as we do not want to insert redundant negated attributes there - // eslint-disable-next-line no-labels - iterationLoop: while ( - this.right !== null && - (length > 0 || - ( - negatedAttributes.size > 0 && - ((this.right.deleted && this.am.contentLength(this.right) === 0) || this.right.content.constructor === ContentFormat) - ) - ) - ) { - switch (this.right.content.constructor) { - case ContentFormat: { - if (!this.right.deleted) { - const { key, value } = /** @type {ContentFormat} */ (this.right.content) - const attr = attributes[key] - if (attr !== undefined) { - if (equalAttrs(attr, value)) { - negatedAttributes.delete(key) - } else { - if (length === 0) { - // no need to further extend negatedAttributes - // eslint-disable-next-line no-labels - break iterationLoop - } - negatedAttributes.set(key, value) - } - this.right.delete(transaction) - } else { - this.currentAttributes.set(key, value) - } - } - break - } - default: { - const item = this.right - const rightLen = this.am.contentLength(item) - if (length < rightLen) { - /** - * @type {Array>} - */ - const contents = [] - this.am.readContent(contents, item.id.client, item.id.clock, item.deleted, item.content, 0) - let i = 0 - for (; i < contents.length && length > 0; i++) { - const c = contents[i] - if ((!c.deleted || c.attrs != null) && c.content.isCountable()) { - length -= c.content.getLength() - } - } - if (length < 0 || (length === 0 && i !== contents.length)) { - const c = contents[--i] - getItemCleanStart(transaction, createID(item.id.client, c.clock + c.content.getLength() + length)) - } - } else { - length -= rightLen - } - break - } - } - this.forward() - } - // Quill just assumes that the editor starts with a newline and that it always - // ends with a newline. We only insert that newline when a new newline is - // inserted - i.e when length is bigger than type.length - if (length > 0) { - let newlines = '' - for (; length > 0; length--) { - newlines += '\n' - } - this.right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), this.left, this.left && this.left.lastId, this.right, this.right && this.right.id, parent, null, new ContentString(newlines)) - this.right.integrate(transaction, 0) - this.forward() - } - insertNegatedAttributes(transaction, parent, this, negatedAttributes) - } -} - -/** - * @param {Transaction} transaction - * @param {ItemTextListPosition} pos - * @param {number} count steps to move forward - * @return {ItemTextListPosition} - * - * @private - * @function - */ -const findNextPosition = (transaction, pos, count) => { - while (pos.right !== null && count > 0) { - switch (pos.right.content.constructor) { - case ContentFormat: - if (!pos.right.deleted) { - updateCurrentAttributes(pos.currentAttributes, /** @type {ContentFormat} */ (pos.right.content)) - } - break - default: - if (!pos.right.deleted) { - if (count < pos.right.length) { - // split right - getItemCleanStart(transaction, createID(pos.right.id.client, pos.right.id.clock + count)) - } - pos.index += pos.right.length - count -= pos.right.length - } - break - } - pos.left = pos.right - pos.right = pos.right.right - // pos.forward() - we don't forward because that would halve the performance because we already do the checks above - } - return pos -} - -/** - * @param {Transaction} transaction - * @param {import('../utils/types.js').YType} parent - * @param {number} index - * @param {boolean} useSearchMarker - * @return {ItemTextListPosition} - * - * @private - * @function - */ -const findPosition = (transaction, parent, index, useSearchMarker) => { - const currentAttributes = new Map() - const marker = useSearchMarker ? findMarker(parent, index) : null - if (marker) { - const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes, noAttributionsManager) - return findNextPosition(transaction, pos, index - marker.index) - } else { - const pos = new ItemTextListPosition(null, parent._start, 0, currentAttributes, noAttributionsManager) - return findNextPosition(transaction, pos, index) - } -} - -/** - * Negate applied formats - * - * @param {Transaction} transaction - * @param {import('../utils/types.js').YType} parent - * @param {ItemTextListPosition} currPos - * @param {Map} negatedAttributes - * - * @private - * @function - */ -const insertNegatedAttributes = (transaction, parent, currPos, negatedAttributes) => { - // check if we really need to remove attributes - while ( - currPos.right !== null && ( - (currPos.right.deleted && (currPos.am === noAttributionsManager || currPos.am.contentLength(currPos.right) === 0)) || ( - currPos.right.content.constructor === ContentFormat && - equalAttrs(negatedAttributes.get(/** @type {ContentFormat} */ (currPos.right.content).key), /** @type {ContentFormat} */ (currPos.right.content).value) - ) - ) - ) { - if (!currPos.right.deleted) { - negatedAttributes.delete(/** @type {ContentFormat} */ (currPos.right.content).key) - } - currPos.forward() - } - const doc = transaction.doc - const ownClientId = doc.clientID - negatedAttributes.forEach((val, key) => { - const left = currPos.left - const right = currPos.right - const nextFormat = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val)) - nextFormat.integrate(transaction, 0) - currPos.right = nextFormat - currPos.forward() - }) -} - -/** - * @param {Map} currentAttributes - * @param {ContentFormat} format - * - * @private - * @function - */ -const updateCurrentAttributes = (currentAttributes, format) => { - const { key, value } = format - if (value === null) { - currentAttributes.delete(key) - } else { - currentAttributes.set(key, value) - } -} - -/** - * @param {ItemTextListPosition} currPos - * @param {Object} attributes - * - * @private - * @function - */ -const minimizeAttributeChanges = (currPos, attributes) => { - // go right while attributes[right.key] === right.value (or right is deleted) - while (true) { - if (currPos.right === null) { - break - } else if (currPos.right.deleted ? (currPos.am.contentLength(currPos.right) === 0) : (!currPos.right.deleted && currPos.right.content.constructor === ContentFormat && equalAttrs(attributes[(/** @type {ContentFormat} */ (currPos.right.content)).key] ?? null, /** @type {ContentFormat} */ (currPos.right.content).value))) { - // - } else { - break - } - currPos.forward() - } -} - -/** - * @param {Transaction} transaction - * @param {import('../utils/types.js').YType} parent - * @param {ItemTextListPosition} currPos - * @param {Object} attributes - * @return {Map} - * - * @private - * @function - **/ -const insertAttributes = (transaction, parent, currPos, attributes) => { - const doc = transaction.doc - const ownClientId = doc.clientID - const negatedAttributes = new Map() - // insert format-start items - for (const key in attributes) { - const val = attributes[key] - const currentVal = currPos.currentAttributes.get(key) ?? null - if (!equalAttrs(currentVal, val)) { - // save negated attribute (set null if currentVal undefined) - negatedAttributes.set(key, currentVal) - const { left, right } = currPos - currPos.right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, new ContentFormat(key, val)) - currPos.right.integrate(transaction, 0) - currPos.forward() - } - } - return negatedAttributes -} - -/** - * @param {Transaction} transaction - * @param {import('../utils/types.js').YType} parent - * @param {ItemTextListPosition} currPos - * @param {string|object|import('../utils/types.js').YType} text - * @param {Object} attributes - * - * @private - * @function - **/ -export const insertText = (transaction, parent, currPos, text, attributes) => { - currPos.currentAttributes.forEach((_val, key) => { - if (attributes[key] === undefined) { - attributes[key] = null - } - }) - const doc = transaction.doc - const ownClientId = doc.clientID - minimizeAttributeChanges(currPos, attributes) - const negatedAttributes = insertAttributes(transaction, parent, currPos, attributes) - // insert content - const content = text.constructor === String ? new ContentString(/** @type {string} */ (text)) : (text instanceof AbstractType ? new ContentType(text) : new ContentEmbed(text)) - let { left, right, index } = currPos - if (parent._searchMarker) { - updateMarkerChanges(parent._searchMarker, currPos.index, content.getLength()) - } - right = new Item(createID(ownClientId, getState(doc.store, ownClientId)), left, left && left.lastId, right, right && right.id, parent, null, content) - right.integrate(transaction, 0) - currPos.right = right - currPos.index = index - currPos.forward() - insertNegatedAttributes(transaction, parent, currPos, negatedAttributes) -} - -/** - * Call this function after string content has been deleted in order to - * clean up formatting Items. - * - * @param {Transaction} transaction - * @param {Item} start - * @param {Item|null} curr exclusive end, automatically iterates to the next Content Item - * @param {Map} startAttributes - * @param {Map} currAttributes - * @return {number} The amount of formatting Items deleted. - * - * @function - */ -const cleanupFormattingGap = (transaction, start, curr, startAttributes, currAttributes) => { - if (!transaction.doc.cleanupFormatting) return 0 - /** - * @type {Item|null} - */ - let end = start - /** - * @type {Map} - */ - const endFormats = map.create() - while (end && (!end.countable || end.deleted)) { - if (!end.deleted && end.content.constructor === ContentFormat) { - const cf = /** @type {ContentFormat} */ (end.content) - endFormats.set(cf.key, cf) - } - end = end.right - } - let cleanups = 0 - let reachedCurr = false - while (start !== end) { - if (curr === start) { - reachedCurr = true - } - if (!start.deleted) { - const content = start.content - switch (content.constructor) { - case ContentFormat: { - const { key, value } = /** @type {ContentFormat} */ (content) - const startAttrValue = startAttributes.get(key) ?? null - if (endFormats.get(key) !== content || startAttrValue === value) { - // Either this format is overwritten or it is not necessary because the attribute already existed. - start.delete(transaction) - transaction.cleanUps.add(start.id.client, start.id.clock, start.length) - cleanups++ - if (!reachedCurr && (currAttributes.get(key) ?? null) === value && startAttrValue !== value) { - if (startAttrValue === null) { - currAttributes.delete(key) - } else { - currAttributes.set(key, startAttrValue) - } - } - } - if (!reachedCurr && !start.deleted) { - updateCurrentAttributes(currAttributes, /** @type {ContentFormat} */ (content)) - } - break - } - } - } - start = /** @type {Item} */ (start.right) - } - return cleanups -} - -/** - * @param {Transaction} transaction - * @param {Item | null} item - */ -const cleanupContextlessFormattingGap = (transaction, item) => { - if (!transaction.doc.cleanupFormatting) return 0 - // iterate until item.right is null or content - while (item && item.right && (item.right.deleted || !item.right.countable)) { - item = item.right - } - const attrs = new Set() - // iterate back until a content item is found - while (item && (item.deleted || !item.countable)) { - if (!item.deleted && item.content.constructor === ContentFormat) { - const key = /** @type {ContentFormat} */ (item.content).key - if (attrs.has(key)) { - item.delete(transaction) - transaction.cleanUps.add(item.id.client, item.id.clock, item.length) - } else { - attrs.add(key) - } - } - item = item.left - } -} - -/** - * This function is experimental and subject to change / be removed. - * - * Ideally, we don't need this function at all. Formatting attributes should be cleaned up - * automatically after each change. This function iterates twice over the complete YText type - * and removes unnecessary formatting attributes. This is also helpful for testing. - * - * This function won't be exported anymore as soon as there is confidence that the YText type works as intended. - * - * @param {YText} type - * @return {number} How many formatting attributes have been cleaned up. - */ -export const cleanupYTextFormatting = type => { - if (!type.doc?.cleanupFormatting) return 0 - let res = 0 - transact(/** @type {Doc} */ (type.doc), transaction => { - let start = /** @type {Item} */ (type._start) - let end = type._start - let startAttributes = map.create() - const currentAttributes = map.copy(startAttributes) - while (end) { - if (end.deleted === false) { - switch (end.content.constructor) { - case ContentFormat: - updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (end.content)) - break - default: - res += cleanupFormattingGap(transaction, start, end, startAttributes, currentAttributes) - startAttributes = map.copy(currentAttributes) - start = end - break - } - } - end = end.right - } - }) - return res -} - -/** - * This will be called by the transaction once the event handlers are called to potentially cleanup - * formatting attributes. - * - * @param {Transaction} transaction - */ -export const cleanupYTextAfterTransaction = transaction => { - /** - * @type {Set>} - */ - const needFullCleanup = new Set() - // check if another formatting item was inserted - const doc = transaction.doc - iterateStructsByIdSet(transaction, transaction.insertSet, (item) => { - if ( - !item.deleted && /** @type {Item} */ (item).content.constructor === ContentFormat && item.constructor !== GC - ) { - needFullCleanup.add(/** @type {any} */ (item).parent) - } - }) - // cleanup in a new transaction - transact(doc, (t) => { - iterateStructsByIdSet(transaction, transaction.deleteSet, item => { - if (item instanceof GC || !(/** @type {YText} */ (item.parent)._hasFormatting) || needFullCleanup.has(/** @type {YText} */ (item.parent))) { - return - } - const parent = /** @type {YText} */ (item.parent) - if (item.content.constructor === ContentFormat) { - needFullCleanup.add(parent) - } else { - // If no formatting attribute was inserted or deleted, we can make due with contextless - // formatting cleanups. - // Contextless: it is not necessary to compute currentAttributes for the affected position. - cleanupContextlessFormattingGap(t, item) - } - }) - // If a formatting item was inserted, we simply clean the whole type. - // We need to compute currentAttributes for the current position anyway. - for (const yText of needFullCleanup) { - cleanupYTextFormatting(yText) - } - }) -} - -/** - * @param {Transaction} transaction - * @param {ItemTextListPosition} currPos - * @param {number} length - * @return {ItemTextListPosition} - * - * @private - * @function - */ -export const deleteText = (transaction, currPos, length) => { - const startLength = length - const startAttrs = map.copy(currPos.currentAttributes) - const start = currPos.right - while (length > 0 && currPos.right !== null) { - if (!currPos.right.deleted) { - switch (currPos.right.content.constructor) { - case ContentType: - case ContentEmbed: - case ContentString: - if (length < currPos.right.length) { - getItemCleanStart(transaction, createID(currPos.right.id.client, currPos.right.id.clock + length)) - } - length -= currPos.right.length - currPos.right.delete(transaction) - break - } - } else if (currPos.am !== noAttributionsManager) { - const item = currPos.right - /** - * @type {Array>} - */ - const contents = [] - currPos.am.readContent(contents, item.id.client, item.id.clock, true, item.content, 0) - for (let i = 0; i < contents.length; i++) { - const c = contents[i] - if (c.content.isCountable() && c.attrs != null) { - // deleting already deleted content. store that information in a meta property, but do - // nothing - const contentLen = math.min(c.content.getLength(), length) - map.setIfUndefined(transaction.meta, 'attributedDeletes', createIdSet).add(item.id.client, c.clock, contentLen) - length -= contentLen - } - } - const lastContent = contents.length > 0 ? contents[contents.length - 1] : null - const nextItemClock = item.id.clock + item.length - const nextContentClock = lastContent != null ? lastContent.clock + lastContent.content.getLength() : nextItemClock - if (nextContentClock < nextItemClock) { - getItemCleanStart(transaction, createID(item.id.client, nextContentClock)) - } - } - currPos.forward() - } - if (start) { - cleanupFormattingGap(transaction, start, currPos.right, startAttrs, currPos.currentAttributes) - } - const parent = /** @type {AbstractType} */ (/** @type {Item} */ (currPos.left || currPos.right).parent) - if (parent._searchMarker) { - updateMarkerChanges(parent._searchMarker, currPos.index, -startLength + length) - } - return currPos -} - -/** - * The Quill Delta format represents changes on a text document with - * formatting information. For more information visit {@link https://quilljs.com/docs/delta/|Quill Delta} - * - * @example - * { - * ops: [ - * { insert: 'Gandalf', attributes: { bold: true } }, - * { insert: ' the ' }, - * { insert: 'Grey', attributes: { color: '#cccccc' } } - * ] - * } - * - */ - -/** - * Attributes that can be assigned to a selection of text. - * - * @example - * { - * bold: true, - * font-size: '40px' - * } - * - * @typedef {Object} TextAttributes - */ - -/** - * Type that represents text with formatting information. - * - * This type replaces y-richtext as this implementation is able to handle - * block formats (format information on a paragraph), embeds (complex elements - * like pictures and videos), and text formats (**bold**, *italic*). - * - * @template {{ [key:string]:any } | import('../utils/types.js').YType} [Embeds={ [key:string]:any } | import('../utils/types.js').YType] - * @extends {AbstractType>} - */ -export class YText extends AbstractType { - /** - * @param {String} [string] The initial value of the YText. - */ - constructor (string) { - super() - /** - * Array of pending operations on this type - * @type {Array?} - */ - this._pending = string !== undefined ? [() => this.insert(0, string)] : [] - /** - * @type {Array|null} - */ - this._searchMarker = [] - /** - * Whether this YText contains formatting attributes. - * This flag is updated when a formatting item is integrated (see ContentFormat.integrate) - */ - this._hasFormatting = false - } - - /** - * Number of characters of this text type. - * - * @type {number} - */ - get length () { - this.doc ?? warnPrematureAccess() - return this._length - } - - /** - * @param {Doc} y - * @param {Item?} item - */ - _integrate (y, item) { - super._integrate(y, item) - try { - /** @type {Array} */ (this._pending).forEach(f => f()) - } catch (e) { - console.error(e) - } - this._pending = null - } - - /** - * Makes a copy of this data type that can be included somewhere else. - * - * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. - * - * @return {YText} - */ - clone () { - /** - * @type {YText} - */ - const text = /** @type {any} */ (new YText()) - text.applyDelta(this.getContent()) - return text - } - - /** - * Creates YTextEvent and calls observers. - * - * @param {Transaction} transaction - * @param {Set} parentSubs Keys changed on this type. `null` if list was modified. - */ - _callObserver (transaction, parentSubs) { - super._callObserver(transaction, parentSubs) - // If a remote change happened, we try to cleanup potential formatting duplicates. - if (!transaction.local && this._hasFormatting) { - transaction._needFormattingCleanup = true - } - } - - /** - * Returns the unformatted string representation of this YText type. - * - * @public - */ - toString () { - this.doc ?? warnPrematureAccess() - let str = '' - /** - * @type {Item|null} - */ - let n = this._start - while (n !== null) { - if (!n.deleted && n.countable && n.content.constructor === ContentString) { - str += /** @type {ContentString} */ (n.content).str - } - n = n.right - } - return str - } - - /** - * Returns the unformatted string representation of this YText type. - * - * @return {string} - * @public - */ - toJSON () { - return this.toString() - } - - /** - * Insert text at a given index. - * - * @param {number} index The index at which to start inserting. - * @param {String} text The text to insert at the specified position. - * @param {TextAttributes} [attributes] Optionally define some formatting - * information to apply on the inserted - * Text. - * @public - */ - insert (index, text, attributes) { - if (text.length <= 0) { - return - } - const y = this.doc - if (y !== null) { - transact(y, transaction => { - const pos = findPosition(transaction, this, index, !attributes) - if (!attributes) { - attributes = {} - // @ts-ignore - pos.currentAttributes.forEach((v, k) => { attributes[k] = v }) - } - insertText(transaction, this, pos, text, attributes) - }) - } else { - /** @type {Array} */ (this._pending).push(() => this.insert(index, text, attributes)) - } - } - - /** - * Inserts an embed at a index. - * - * @param {number} index The index to insert the embed at. - * @param {Object | AbstractType} embed The Object that represents the embed. - * @param {TextAttributes} [attributes] Attribute information to apply on the - * embed - * - * @public - */ - insertEmbed (index, embed, attributes) { - const y = this.doc - if (y !== null) { - transact(y, transaction => { - const pos = findPosition(transaction, this, index, !attributes) - insertText(transaction, this, pos, embed, attributes || {}) - }) - } else { - /** @type {Array} */ (this._pending).push(() => this.insertEmbed(index, embed, attributes || {})) - } - } - - /** - * Deletes text starting from an index. - * - * @param {number} index Index at which to start deleting. - * @param {number} length The number of characters to remove. Defaults to 1. - * - * @public - */ - delete (index, length) { - if (length === 0) { - return - } - const y = this.doc - if (y !== null) { - transact(y, transaction => { - deleteText(transaction, findPosition(transaction, this, index, true), length) - }) - } else { - /** @type {Array} */ (this._pending).push(() => this.delete(index, length)) - } - } - - /** - * Assigns properties to a range of text. - * - * @param {number} index The position where to start formatting. - * @param {number} length The amount of characters to assign properties to. - * @param {TextAttributes} attributes Attribute information to apply on the - * text. - * - * @public - */ - format (index, length, attributes) { - if (length === 0) { - return - } - const y = this.doc - if (y !== null) { - transact(y, transaction => { - const pos = findPosition(transaction, this, index, false) - if (pos.right === null) { - return - } - pos.formatText(transaction, this, length, attributes) - }) - } else { - /** @type {Array} */ (this._pending).push(() => this.format(index, length, attributes)) - } - } - - /** - * Removes an attribute. - * - * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. - * - * @param {String} attributeName The attribute name that is to be removed. - * - * @public - */ - removeAttribute (attributeName) { - if (this.doc !== null) { - transact(this.doc, transaction => { - typeMapDelete(transaction, this, attributeName) - }) - } else { - /** @type {Array} */ (this._pending).push(() => this.removeAttribute(attributeName)) - } - } - - /** - * Sets or updates an attribute. - * - * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. - * - * @param {String} attributeName The attribute name that is to be set. - * @param {any} attributeValue The attribute value that is to be set. - * - * @public - */ - setAttribute (attributeName, attributeValue) { - if (this.doc !== null) { - transact(this.doc, transaction => { - typeMapSet(transaction, this, attributeName, attributeValue) - }) - } else { - /** @type {Array} */ (this._pending).push(() => this.setAttribute(attributeName, attributeValue)) - } - } - - /** - * Returns an attribute value that belongs to the attribute name. - * - * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. - * - * @param {String} attributeName The attribute name that identifies the - * queried value. - * @return {any} The queried attribute value. - * - * @public - */ - getAttribute (attributeName) { - return /** @type {any} */ (typeMapGet(this, attributeName)) - } - - /** - * Returns all attribute name/value pairs in a JSON Object. - * - * @note Xml-Text nodes don't have attributes. You can use this feature to assign properties to complete text-blocks. - * - * @return {Object} A JSON Object that describes the attributes. - * - * @public - */ - getAttributes () { - return typeMapGetAll(this) - } - - /** - * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder - */ - _write (encoder) { - encoder.writeTypeRef(YTextRefID) - } - - /** - * @param {this} other - */ - [traits.EqualityTraitSymbol] (other) { - return this.getContent().equals(other.getContent()) - } -} - -/** - * @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder - * @return {import('../utils/types.js').YType} - * - * @private - * @function - */ -export const readYText = _decoder => new YText() diff --git a/src/types/YXmlElement.js b/src/types/YXmlElement.js deleted file mode 100644 index 83a3aa4a..00000000 --- a/src/types/YXmlElement.js +++ /dev/null @@ -1,227 +0,0 @@ -import * as object from 'lib0/object' - -import { - YXmlFragment, - transact, - typeMapDelete, - typeMapHas, - typeMapSet, - typeMapGet, - typeMapGetAll, - typeMapGetAllSnapshot, - YXmlElementRefID, - Snapshot, YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item, // eslint-disable-line -} from '../internals.js' - -/** - * @typedef {Object|number|null|Array|string|Uint8Array|AbstractType} ValueTypes - */ - -/** - * An YXmlElement imitates the behavior of a - * https://developer.mozilla.org/en-US/docs/Web/API/Element|Dom Element - * - * * An YXmlElement has attributes (key value pairs) - * * An YXmlElement has childElements that must inherit from YXmlElement - * - * @template {{ [key: string]: any }} [Attrs={ [key: string]: string }] - * @template {any} [Children=any] - * @extends YXmlFragment - */ -export class YXmlElement extends YXmlFragment { - constructor (nodeName = 'UNDEFINED') { - super() - this.nodeName = nodeName - /** - * @type {Map|null} - */ - this._prelimAttrs = new Map() - } - - /** - * @type {YXmlElement|YXmlText|null} - */ - get nextSibling () { - const n = this._item ? this._item.next : null - return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null - } - - /** - * @type {YXmlElement|YXmlText|null} - */ - get prevSibling () { - const n = this._item ? this._item.prev : null - return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null - } - - /** - * Integrate this type into the Yjs instance. - * - * * Save this struct in the os - * * This type is sent to other client - * * Observer functions are fired - * - * @param {Doc} y The Yjs instance - * @param {Item?} item - */ - _integrate (y, item) { - super._integrate(y, item) - ;(/** @type {Map} */ (this._prelimAttrs)).forEach((value, key) => { - this.setAttribute(key, value) - }) - this._prelimAttrs = null - } - - /** - * Creates an Item with the same effect as this Item (without position effect) - * - * @return {this} - */ - _copy () { - return /** @type {any} */ (new YXmlElement(this.nodeName)) - } - - /** - * Makes a copy of this data type that can be included somewhere else. - * - * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. - * - * @return {this} - */ - clone () { - const el = this._copy() - const attrs = this.getAttributes() - object.forEach(attrs, (value, key) => { - if (typeof value === 'string') { - el.setAttribute(key, value) - } - }) - // @ts-ignore - el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item)) - return el - } - - /** - * Returns the XML serialization of this YXmlElement. - * The attributes are ordered by attribute-name, so you can easily use this - * method to compare YXmlElements - * - * @return {string} The string representation of this type. - * - * @public - */ - toString () { - const attrs = this.getAttributes() - const stringBuilder = [] - const keys = [] - for (const key in attrs) { - keys.push(key) - } - keys.sort() - const keysLen = keys.length - for (let i = 0; i < keysLen; i++) { - const key = keys[i] - stringBuilder.push(key + '="' + attrs[key] + '"') - } - const nodeName = this.nodeName.toLocaleLowerCase() - const attrsString = stringBuilder.length > 0 ? ' ' + stringBuilder.join(' ') : '' - return `<${nodeName}${attrsString}>${super.toString()}` - } - - /** - * Removes an attribute from this YXmlElement. - * - * @param {string} attributeName The attribute name that is to be removed. - * - * @public - */ - removeAttribute (attributeName) { - if (this.doc !== null) { - transact(this.doc, transaction => { - typeMapDelete(transaction, this, attributeName) - }) - } else { - /** @type {Map} */ (this._prelimAttrs).delete(attributeName) - } - } - - /** - * Sets or updates an attribute. - * - * @template {keyof Attrs & string} KEY - * - * @param {KEY} attributeName The attribute name that is to be set. - * @param {Attrs[KEY]} attributeValue The attribute value that is to be set. - * - * @public - */ - setAttribute (attributeName, attributeValue) { - if (this.doc !== null) { - transact(this.doc, transaction => { - typeMapSet(transaction, this, attributeName, /** @type {any} */ (attributeValue)) - }) - } else { - /** @type {Map} */ (this._prelimAttrs).set(attributeName, attributeValue) - } - } - - /** - * Returns an attribute value that belongs to the attribute name. - * - * @template {keyof Attrs & string} KEY - * - * @param {KEY} attributeName The attribute name that identifies the - * queried value. - * @return {Attrs[KEY]|undefined} The queried attribute value. - * - * @public - */ - getAttribute (attributeName) { - return /** @type {any} */ (typeMapGet(this, attributeName)) - } - - /** - * Returns whether an attribute exists - * - * @param {string} attributeName The attribute name to check for existence. - * @return {boolean} whether the attribute exists. - * - * @public - */ - hasAttribute (attributeName) { - return /** @type {any} */ (typeMapHas(this, attributeName)) - } - - /** - * Returns all attribute name/value pairs in a JSON Object. - * - * @param {Snapshot} [snapshot] - * @return {{ [Key in Extract]?: Attrs[Key]}} A JSON Object that describes the attributes. - * - * @public - */ - getAttributes (snapshot) { - return /** @type {any} */ (snapshot ? typeMapGetAllSnapshot(this, snapshot) : typeMapGetAll(this)) - } - - /** - * Transform the properties of this type to binary and write it to an - * BinaryEncoder. - * - * This is called when this Item is sent to a remote peer. - * - * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. - */ - _write (encoder) { - encoder.writeTypeRef(YXmlElementRefID) - encoder.writeKey(this.nodeName) - } -} - -/** - * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder - * @return {import('../utils/types.js').YType} - * - * @function - */ -export const readYXmlElement = decoder => new YXmlElement(decoder.readKey()) diff --git a/src/types/YXmlFragment.js b/src/types/YXmlFragment.js deleted file mode 100644 index be65a48f..00000000 --- a/src/types/YXmlFragment.js +++ /dev/null @@ -1,266 +0,0 @@ -/** - * @module YXml - */ - -import { - AbstractType, - typeListMap, - typeListForEach, - typeListInsertGenerics, - typeListInsertGenericsAfter, - typeListDelete, - typeListToArray, - YXmlFragmentRefID, - transact, - typeListGet, - typeListSlice, - warnPrematureAccess, - YXmlElement, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, YXmlText, YXmlHook // eslint-disable-line -} from '../internals.js' - -import * as delta from 'lib0/delta' // eslint-disable-line -import * as error from 'lib0/error' - -/** - * Define the elements to which a set of CSS queries apply. - * {@link https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Selectors|CSS_Selectors} - * - * @example - * query = '.classSelector' - * query = 'nodeSelector' - * query = '#idSelector' - * - * @typedef {string} CSS_Selector - */ - -/** - * Dom filter function. - * - * @callback domFilter - * @param {string} nodeName The nodeName of the element - * @param {Map} attributes The map of attributes. - * @return {boolean} Whether to include the Dom node in the YXmlElement. - */ - -/** - * Represents a list of {@link YXmlElement}.and {@link YXmlText} types. - * A YxmlFragment is similar to a {@link YXmlElement}, but it does not have a - * nodeName and it does not have attributes. Though it can be bound to a DOM - * element - in this case the attributes and the nodeName are not shared. - * - * @public - * @template {any} [Children=any] - * @template {{[K in string]:any}} [Attrs={}] - * @extends AbstractType> - */ -export class YXmlFragment extends AbstractType { - constructor () { - super() - /** - * @todo remove _prelimContent - * @type {Array|null} - */ - this._prelimContent = [] - } - - /** - * @type {YXmlElement|YXmlText|null} - */ - get firstChild () { - const first = this._first - return first ? first.content.getContent()[0] : null - } - - /** - * Integrate this type into the Yjs instance. - * - * * Save this struct in the os - * * This type is sent to other client - * * Observer functions are fired - * - * @param {Doc} y The Yjs instance - * @param {Item?} item - */ - _integrate (y, item) { - super._integrate(y, item) - this.insert(0, /** @type {Array} */ (this._prelimContent)) - this._prelimContent = null - } - - /** - * Makes a copy of this data type that can be included somewhere else. - * - * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. - * - * @return {this} - */ - clone () { - const el = this._copy() - el.insert(0, this.toArray().map(item => item instanceof AbstractType ? item.clone() : item)) - return el - } - - get length () { - this.doc ?? warnPrematureAccess() - return this._prelimContent === null ? this._length : this._prelimContent.length - } - - /** - * Get the string representation of all the children of this YXmlFragment. - * - * @return {string} The string representation of all children. - */ - toString () { - return typeListMap(this, xml => xml.toString()).join('') - } - - /** - * @return {string} - */ - toJSON () { - return this.toString() - } - - /** - * Inserts new content at an index. - * - * @example - * // Insert character 'a' at position 0 - * xml.insert(0, [new Y.XmlText('text')]) - * - * @param {number} index The index to insert content at - * @param {Array} content The array of content - */ - insert (index, content) { - if (this.doc !== null) { - transact(this.doc, transaction => { - typeListInsertGenerics(transaction, this, index, content) - }) - } else { - // @ts-ignore _prelimContent is defined because this is not yet integrated - this._prelimContent.splice(index, 0, ...content) - } - } - - /** - * Inserts new content at an index. - * - * @example - * // Insert character 'a' at position 0 - * xml.insert(0, [new Y.XmlText('text')]) - * - * @param {null|Item|YXmlElement|YXmlText} ref The index to insert content at - * @param {Array} content The array of content - */ - insertAfter (ref, content) { - if (this.doc !== null) { - transact(this.doc, transaction => { - const refItem = (ref && ref instanceof AbstractType) ? ref._item : ref - typeListInsertGenericsAfter(transaction, this, refItem, content) - }) - } else { - const pc = /** @type {Array} */ (this._prelimContent) - const index = ref === null ? 0 : pc.findIndex(el => el === ref) + 1 - if (index === 0 && ref !== null) { - throw error.create('Reference item not found') - } - pc.splice(index, 0, ...content) - } - } - - /** - * Deletes elements starting from an index. - * - * @param {number} index Index at which to start deleting elements - * @param {number} [length=1] The number of elements to remove. Defaults to 1. - */ - delete (index, length = 1) { - if (this.doc !== null) { - transact(this.doc, transaction => { - typeListDelete(transaction, this, index, length) - }) - } else { - // @ts-ignore _prelimContent is defined because this is not yet integrated - this._prelimContent.splice(index, length) - } - } - - /** - * Transforms this YArray to a JavaScript Array. - * - * @return {Array} - */ - toArray () { - return typeListToArray(this) - } - - /** - * Appends content to this YArray. - * - * @param {Array} content Array of content to append. - */ - push (content) { - this.insert(this.length, content) - } - - /** - * Prepends content to this YArray. - * - * @param {Array} content Array of content to prepend. - */ - unshift (content) { - this.insert(0, content) - } - - /** - * Returns the i-th element from a YArray. - * - * @param {number} index The index of the element to return from the YArray - * @return {YXmlElement|YXmlText} - */ - get (index) { - return typeListGet(this, index) - } - - /** - * Returns a portion of this YXmlFragment into a JavaScript Array selected - * from start to end (end not included). - * - * @param {number} [start] - * @param {number} [end] - * @return {Array} - */ - slice (start = 0, end = this.length) { - return typeListSlice(this, start, end) - } - - /** - * Executes a provided function on once on every child element. - * - * @param {function(YXmlElement|YXmlText,number, typeof self):void} f A function to execute on every element of this YArray. - */ - forEach (f) { - typeListForEach(this, f) - } - - /** - * Transform the properties of this type to binary and write it to an - * BinaryEncoder. - * - * This is called when this Item is sent to a remote peer. - * - * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. - */ - _write (encoder) { - encoder.writeTypeRef(YXmlFragmentRefID) - } -} - -/** - * @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder - * @return {import('../utils/types.js').YType} - * - * @private - * @function - */ -export const readYXmlFragment = _decoder => new YXmlFragment() diff --git a/src/types/YXmlHook.js b/src/types/YXmlHook.js deleted file mode 100644 index 13e49be2..00000000 --- a/src/types/YXmlHook.js +++ /dev/null @@ -1,68 +0,0 @@ -import { - YMap, - YXmlHookRefID, - UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2 // eslint-disable-line -} from '../internals.js' - -/** - * You can manage binding to a custom type with YXmlHook. - * - * @extends {YMap} - */ -export class YXmlHook extends YMap { - /** - * @param {string} hookName nodeName of the Dom Node. - */ - constructor (hookName) { - super() - /** - * @type {string} - */ - this.hookName = hookName - } - - /** - * @return {this} - */ - _copy () { - return /** @type {this} */ (new YXmlHook(this.hookName)) - } - - /** - * Makes a copy of this data type that can be included somewhere else. - * - * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. - * - * @return {this} - */ - clone () { - const el = this._copy() - this.forEach((value, key) => { - el.set(key, value) - }) - return el - } - - /** - * Transform the properties of this type to binary and write it to an - * BinaryEncoder. - * - * This is called when this Item is sent to a remote peer. - * - * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder The encoder to write data to. - */ - _write (encoder) { - encoder.writeTypeRef(YXmlHookRefID) - encoder.writeKey(this.hookName) - } -} - -/** - * @param {UpdateDecoderV1 | UpdateDecoderV2} decoder - * @return {import('../utils/types.js').YType} - * - * @private - * @function - */ -export const readYXmlHook = decoder => - new YXmlHook(decoder.readKey()) diff --git a/src/types/YXmlText.js b/src/types/YXmlText.js deleted file mode 100644 index 8f66ad11..00000000 --- a/src/types/YXmlText.js +++ /dev/null @@ -1,66 +0,0 @@ -import { - YText, - YXmlTextRefID, - ContentType, YXmlElement, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, // eslint-disable-line -} from '../internals.js' - -/** - * @todo can we deprecate this? - * - * Represents text in a Dom Element. In the future this type will also handle - * simple formatting information like bold and italic. - * @extends YText - */ -export class YXmlText extends YText { - /** - * @type {YXmlElement|YXmlText|null} - */ - get nextSibling () { - const n = this._item ? this._item.next : null - return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null - } - - /** - * @type {YXmlElement|YXmlText|null} - */ - get prevSibling () { - const n = this._item ? this._item.prev : null - return n ? /** @type {YXmlElement|YXmlText} */ (/** @type {ContentType} */ (n.content).type) : null - } - - /** - * Makes a copy of this data type that can be included somewhere else. - * - * Note that the content is only readable _after_ it has been included somewhere in the Ydoc. - * - * @return {this} - */ - clone () { - const text = /** @type {this} */ (this._copy()) - text.applyDelta(this.getContent()) - return text - } - - /** - * @return {string} - */ - toJSON () { - return this.toString() - } - - /** - * @param {UpdateEncoderV1 | UpdateEncoderV2} encoder - */ - _write (encoder) { - encoder.writeTypeRef(YXmlTextRefID) - } -} - -/** - * @param {UpdateDecoderV1 | UpdateDecoderV2} _decoder - * @return {import('../utils/types.js').YType} - * - * @private - * @function - */ -export const readYXmlText = _decoder => new YXmlText() diff --git a/src/utils/Doc.js b/src/utils/Doc.js index a15ccd9b..4e8edf5e 100644 --- a/src/utils/Doc.js +++ b/src/utils/Doc.js @@ -4,13 +4,13 @@ import { StructStore, - YType, transact, applyUpdate, ContentDoc, Item, Transaction, // eslint-disable-line encodeStateAsUpdate } from '../internals.js' +import { YType } from '../ytype.js' import { ObservableV2 } from 'lib0/observable' import * as random from 'lib0/random' import * as map from 'lib0/map' @@ -190,22 +190,18 @@ export class Doc extends ObservableV2 { /** * Define a shared data type. * - * Multiple calls of `ydoc.get(name, TypeConstructor)` yield the same result + * Multiple calls of `ydoc.get(name)` yield the same result * and do not overwrite each other. I.e. - * `ydoc.get(name, Y.Array) === ydoc.get(name, Y.Array)` + * `ydoc.get(name) === ydoc.get(name)` * * After this method is called, the type is also available on `ydoc.share.get(name)`. * - * *Best Practices:* - * Define all types right after the Y.Doc instance is created and store them in a separate object. - * Also use the typed methods `getText(name)`, `getArray(name)`, .. - * * @param {string} key * @param {string?} name Type-name * * @return {YType} */ - get (key, name = null) { + get (key = '', name = null) { return map.setIfUndefined(this.share, key, () => { const t = new YType(name) t._integrate(this, null) diff --git a/src/utils/Transaction.js b/src/utils/Transaction.js index 8734aa26..9b2e7b8a 100644 --- a/src/utils/Transaction.js +++ b/src/utils/Transaction.js @@ -14,7 +14,7 @@ import { IdSet, UpdateEncoderV1, UpdateEncoderV2, GC, StructStore, AbstractStruct, YEvent, Doc // eslint-disable-line } from '../internals.js' -import {YType} from '../ytype.js' +import { YType } from '../ytype.js' // eslint-disable-line import * as error from 'lib0/error' import * as map from 'lib0/map' import * as math from 'lib0/math' @@ -343,7 +343,6 @@ const updateCurrentAttributes = (currentAttributes, { key, value }) => { } } - /** * Call this function after string content has been deleted in order to * clean up formatting Items. @@ -411,7 +410,6 @@ export const cleanupFormattingGap = (transaction, start, curr, startAttributes, return cleanups } - /** * This function is experimental and subject to change / be removed. * @@ -451,7 +449,6 @@ export const cleanupYTextFormatting = type => { return res } - /** * This will be called by the transaction once the event handlers are called to potentially cleanup * formatting attributes. diff --git a/src/utils/YEvent.js b/src/utils/YEvent.js index 9471fe15..cf8c6a4c 100644 --- a/src/utils/YEvent.js +++ b/src/utils/YEvent.js @@ -39,7 +39,7 @@ export class YEvent { */ this.transaction = transaction /** - * @type {import('../ytype.js').DeltaConfTypesToDelta|null} + * @type {delta.Delta>|null} */ this._delta = null /** @@ -116,7 +116,7 @@ export class YEvent { * @param {AbstractAttributionManager} am * @param {object} [opts] * @param {Deep} [opts.deep] - * @return {Deep extends true ? delta.Delta> : delta.Delta} The Delta representation of this type. + * @return {Deep extends true ? delta.Delta : delta.Delta>} The Delta representation of this type. * * @public */ @@ -155,7 +155,7 @@ export class YEvent { * Compute the changes in the delta format. * A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document. * - * @type {delta.Delta} The Delta representation of this type. + * @type {delta.Delta>} The Delta representation of this type. * @public */ get delta () { @@ -166,7 +166,7 @@ export class YEvent { * Compute the changes in the delta format. * A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document. * - * @type {import('../internals.js').DeltaConfTypesToDeltaDelta} The Delta representation of this type. + * @type {delta.Delta} The Delta representation of this type. * @public */ get deltaDeep () { @@ -200,7 +200,7 @@ export const getPathTo = (parent, child, am = noAttributionsManager) => { // parent is map-ish path.unshift(child._item.parentSub) } else { - const parent = /** @type {import('../utils/types.js').YType} */ (child._item.parent) + const parent = /** @type {import('../ytype.js').YType} */ (child._item.parent) // parent is array-ish const apos = /** @type {AbsolutePosition} */ (createAbsolutePositionFromRelativePosition(createRelativePosition(parent, child._item.id), doc, false, am)) path.unshift(apos.index) diff --git a/src/utils/types.js b/src/utils/ts.js similarity index 58% rename from src/utils/types.js rename to src/utils/ts.js index 3eb47b1f..7d4b79b9 100644 --- a/src/utils/types.js +++ b/src/utils/ts.js @@ -1,3 +1,3 @@ /** - * @typedef {Object|Array|number|null|string|Uint8Array|BigInt|import('./').YType} YValue + * @typedef {Object|Array|number|null|string|Uint8Array|BigInt|import('../ytype.js').YType} YValue */ diff --git a/src/utils/updates.js b/src/utils/updates.js index 5f026ad2..4c38fcc9 100644 --- a/src/utils/updates.js +++ b/src/utils/updates.js @@ -584,13 +584,13 @@ export const convertUpdateFormat = (update, blockTransformer, YDecoder, YEncoder * @typedef {Object} ObfuscatorOptions * @property {boolean} [ObfuscatorOptions.formatting=true] * @property {boolean} [ObfuscatorOptions.subdocs=true] - * @property {boolean} [ObfuscatorOptions.yxml=true] Whether to obfuscate nodeName / hookName + * @property {boolean} [ObfuscatorOptions.name=true] Whether to obfuscate nodeName / hookName */ /** * @param {ObfuscatorOptions} obfuscator */ -const createObfuscator = ({ formatting = true, subdocs = true, yxml = true } = {}) => { +const createObfuscator = ({ formatting = true, subdocs = true, name = true } = {}) => { let i = 0 const mapKeyCache = map.create() const nodeNameCache = map.create() @@ -613,10 +613,10 @@ const createObfuscator = ({ formatting = true, subdocs = true, yxml = true } = { case ContentDeleted: break case ContentType: { - if (yxml) { + if (name) { const type = /** @type {ContentType} */ (content).type if (type.name != null) { - type.name = map.setIfUndefined(nodeNameCache, type.nodeName, () => 'typename-' + i) + type.name = map.setIfUndefined(nodeNameCache, type.name, () => 'typename-' + i) } } break diff --git a/src/ytype.js b/src/ytype.js index 04c9c485..6663c367 100644 --- a/src/ytype.js +++ b/src/ytype.js @@ -184,63 +184,6 @@ export class ItemTextListPosition { } } -/** - * @param {Transaction} transaction - * @param {ItemTextListPosition} pos - * @param {number} count steps to move forward - * @return {ItemTextListPosition} - * - * @private - * @function - */ -const findNextPosition = (transaction, pos, count) => { - while (pos.right !== null && count > 0) { - switch (pos.right.content.constructor) { - case ContentFormat: - if (!pos.right.deleted) { - updateCurrentAttributes(pos.currentAttributes, /** @type {ContentFormat} */ (pos.right.content)) - } - break - default: - if (!pos.right.deleted) { - if (count < pos.right.length) { - // split right - getItemCleanStart(transaction, createID(pos.right.id.client, pos.right.id.clock + count)) - } - pos.index += pos.right.length - count -= pos.right.length - } - break - } - pos.left = pos.right - pos.right = pos.right.right - // pos.forward() - we don't forward because that would halve the performance because we already do the checks above - } - return pos -} - -/** - * @param {Transaction} transaction - * @param {YType} parent - * @param {number} index - * @param {boolean} useSearchMarker - * @return {ItemTextListPosition} - * - * @private - * @function - */ -const findPosition = (transaction, parent, index, useSearchMarker) => { - const currentAttributes = new Map() - const marker = useSearchMarker ? findMarker(parent, index) : null - if (marker) { - const pos = new ItemTextListPosition(marker.p.left, marker.p, marker.index, currentAttributes, noAttributionsManager) - return findNextPosition(transaction, pos, index - marker.index) - } else { - const pos = new ItemTextListPosition(null, parent._start, 0, currentAttributes, noAttributionsManager) - return findNextPosition(transaction, pos, index) - } -} - /** * Negate applied formats * @@ -696,12 +639,22 @@ export class YType { this._hasFormatting = false } + /** + * @template {delta.DeltaConf} DC + * @param {delta.Delta} d + * @return {YType} + */ + static from (d) { + const yt = new YType(d.name) + yt.applyDelta(d) + return yt + } + get length () { this.doc ?? warnPrematureAccess() return this._length } - /** * Returns a fresh delta that can be used to change this YType. * @type {delta.DeltaBuilder>} @@ -1075,13 +1028,7 @@ export class YType { for (let i = 0; i < op.insert.length; i++) { let ins = op.insert[i] if (delta.$deltaAny.check(ins)) { - if (ins.name != null) { - const t = new YType(ins.name) - t.applyDelta(ins) - ins = t - } else { - error.unexpectedCase() - } + ins = YType.from(ins) } insertText(transaction, /** @type {any} */ (this), currPos, ins, op.format || {}) } @@ -1103,7 +1050,7 @@ export class YType { for (const op of d.attrs) { if (delta.$setAttrOp.check(op)) { typeMapSet(transaction, /** @type {any} */ (this), /** @type {any} */ (op.key), op.value) - } else if (delta.$deleteOp.check(op)) { + } else if (delta.$deleteAttrOp.check(op)) { typeMapDelete(transaction, /** @type {any} */ (this), /** @type {any} */ (op.key)) } else { const sub = typeMapGet(/** @type {any} */ (this), /** @type {any} */ (op.key)) @@ -1115,6 +1062,7 @@ export class YType { } }) } + return this } /** @@ -1134,14 +1082,13 @@ export class YType { * Removes all elements from this YMap. */ clearAttrs () { - let d = delta.create() - this.forEachAttr((_,key) => { + const d = delta.create() + this.forEachAttr((_, key) => { d.deleteAttr(/** @type {any} */ (key)) }) this.applyDelta(d) } - /** * Removes an attribute from this YXmlElement. * @@ -1163,8 +1110,9 @@ export class YType { * * @public */ - setAttribute (attributeName, attributeValue) { + setAttr (attributeName, attributeValue) { this.applyDelta(delta.create().setAttr(attributeName, attributeValue).done()) + return attributeValue } /** @@ -1175,7 +1123,7 @@ export class YType { * @return {delta.DeltaConfGetAttrs[KEY]|undefined} The queried attribute value. * @public */ - getAttribute (attributeName) { + getAttr (attributeName) { return /** @type {any} */ (typeMapGet(this, attributeName)) } @@ -1187,7 +1135,7 @@ export class YType { * * @public */ - hasAttribute (attributeName) { + hasAttr (attributeName) { return /** @type {any} */ (typeMapHas(this, attributeName)) } @@ -1199,7 +1147,7 @@ export class YType { * * @public */ - getAttributes (snapshot) { + getAttrs (snapshot) { return /** @type {any} */ (snapshot ? typeMapGetAllSnapshot(this, snapshot) : typeMapGetAll(this)) } @@ -1218,9 +1166,54 @@ export class YType { * * @param {number} index The index to insert content at. * @param {Array>|delta.DeltaConfGetText} content Array of content to append. + * @param {delta.FormattingAttributes} [format] */ - insert (index, content) { - this.applyDelta(delta.create().retain(index).insert(/** @type {any} */ (content))) + insert (index, content, format) { + this.applyDelta(delta.create().retain(index).insert(/** @type {any} */ (content), format)) + } + + /** + * Inserts new content at an index. + * + * Important: This function expects an array of content. Not just a content + * object. The reason for this "weirdness" is that inserting several elements + * is very efficient when it is done as a single operation. + * + * @example + * // Insert character 'a' at position 0 + * yarray.insert(0, ['a']) + * // Insert numbers 1, 2 at position 1 + * yarray.insert(1, [1, 2]) + * + * @param {number} index The index to insert content at. + * @param {number} length The index to insert content at. + * @param {delta.FormattingAttributes} formats + * + */ + format (index, length, formats) { + this.applyDelta(delta.create().retain(index).retain(length, formats)) + } + + /** + * Inserts new content after another element. + * + * @example + * // Insert character 'a' at position 0 + * xml.insert(0, [new Y.XmlText('text')]) + * + * @param {null|Item|YType} ref The index to insert content at + * @param {Array>} content The array of content + */ + insertAfter (ref, content) { + if (this.doc !== null) { + transact(this.doc, transaction => { + const refItem = ref && ref instanceof YType ? ref._item : ref + typeListInsertGenericsAfter(transaction, this, refItem, content) + }) + } else { + // only possible once this item has been integrated + error.unexpectedCase() + } } /** @@ -1275,21 +1268,38 @@ export class YType { return typeListSlice(this, start, end) } - /** + * @todo refactor this, this should use getContent only! + * * Transforms this YArray to a JavaScript Array. * - * @return {Array>} + * @return {Array | delta.DeltaConfGetText>} */ toArray () { - return typeListToArray(this) + const dcontent = this.getContent() + /** + * @type {Array} + */ + const children = [] + for (const child of dcontent.children) { + if (delta.$insertOp.check(child) || delta.$textOp.check(child)) { + children.push(child.insert) + } + } + return children } /** * Transforms this Shared Type to a JSON object. */ toJSON () { - return this.getContent().toJSON() + const attrs = this.getAttrs() + const children = this.toArray() + return { + name: this.name, + children, + attrs + } } /** @@ -1297,22 +1307,21 @@ export class YType { * child-element. * * @template M - * @param {(child:delta.DeltaConfGetChildren,index:number,ytype:this)=>M} f Function that produces an element of the new Array + * @param {(child:delta.DeltaConfGetChildren|delta.DeltaConfGetText,index:number)=>M} f Function that produces an element of the new Array * @return {Array} A new array with each element being the result of the * callback function */ map (f) { - return typeListMap(this, /** @type {any} */ (f)) + return this.toArray().map(f) } /** * Executes a provided function once on every element of this YArray. * - * @template M - * @param {(child:delta.DeltaConfGetChildren,index:number,ytype:this)=>M} f Function that produces an element of the new Array + * @param {(child:delta.DeltaConfGetChildren|delta.DeltaConfGetText,index:number)=>any} f Function that produces an element of the new Array */ forEach (f) { - typeListForEach(this, f) + return this.toArray().forEach(f) } /** @@ -1328,19 +1337,10 @@ export class YType { }) } - - - /** - * @return {IterableIterator>} - */ - [Symbol.iterator] () { - return typeListCreateIterator(this) - } - /** * Returns the keys for each element in the YMap Type. * - * @return {IterableIterator>} + * @return {IterableIterator>>} */ attrKeys () { return iterator.iteratorMap(createMapIterator(this), /** @param {any} v */ v => v[0]) @@ -1358,7 +1358,7 @@ export class YType { /** * Returns an Iterator of [key, value] pairs * - * @return {IterableIterator<{ [K in keyof delta.DeltaConfGetAttrs]: [K,delta.DeltaConfGetAttrs] }[any]>} + * @return {IterableIterator<{ [K in keyof delta.DeltaConfGetAttrs]: [K,delta.DeltaConfGetAttrs[K]] }[any]>} */ attrEntries () { return iterator.iteratorMap(createMapIterator(this), /** @param {any} v */ v => /** @type {any} */ ([v[0], v[1].content.getContent()[v[1].length - 1]])) @@ -1369,7 +1369,7 @@ export class YType { * * @return {number} */ - attrSize () { + get attrSize () { return [...createMapIterator(this)].length } @@ -1379,7 +1379,7 @@ export class YType { [traits.EqualityTraitSymbol] (other) { return this.getContent().equals(other.getContent()) } - + /** * @todo this doesn't need to live in a method. * @@ -1475,145 +1475,6 @@ export const typeListSlice = (type, start, end) => { return cs } -/** - * @param {YType} type - * @return {Array} - * - * @private - * @function - */ -export const typeListToArray = type => { - type.doc ?? warnPrematureAccess() - const cs = [] - let n = type._start - while (n !== null) { - if (n.countable && !n.deleted) { - const c = n.content.getContent() - for (let i = 0; i < c.length; i++) { - cs.push(c[i]) - } - } - n = n.right - } - return cs -} - -/** - * @param {YType} type - * @param {Snapshot} snapshot - * @return {Array} - * - * @private - * @function - */ -export const typeListToArraySnapshot = (type, snapshot) => { - const cs = [] - let n = type._start - while (n !== null) { - if (n.countable && isVisible(n, snapshot)) { - const c = n.content.getContent() - for (let i = 0; i < c.length; i++) { - cs.push(c[i]) - } - } - n = n.right - } - return cs -} - -/** - * Executes a provided function on once on every element of this YArray. - * - * @param {YType} type - * @param {function(any,number,any):void} f A function to execute on every element of this YArray. - * - * @private - * @function - */ -export const typeListForEach = (type, f) => { - let index = 0 - let n = type._start - type.doc ?? warnPrematureAccess() - while (n !== null) { - if (n.countable && !n.deleted) { - const c = n.content.getContent() - for (let i = 0; i < c.length; i++) { - f(c[i], index++, type) - } - } - n = n.right - } -} - -/** - * @template C,R - * @param {YType} type - * @param {function(C,number,YType):R} f - * @return {Array} - * - * @private - * @function - */ -export const typeListMap = (type, f) => { - /** - * @type {Array} - */ - const result = [] - typeListForEach(type, (c, i) => { - result.push(f(c, i, type)) - }) - return result -} - -/** - * @param {YType} type - * @return {IterableIterator} - * - * @private - * @function - */ -export const typeListCreateIterator = type => { - let n = type._start - /** - * @type {Array|null} - */ - let currentContent = null - let currentContentIndex = 0 - return { - [Symbol.iterator] () { - return this - }, - next: () => { - // find some content - if (currentContent === null) { - while (n !== null && n.deleted) { - n = n.right - } - // check if we reached the end, no need to check currentContent, because it does not exist - if (n === null) { - return { - done: true, - value: undefined - } - } - // we found n, so we can set currentContent - currentContent = n.content.getContent() - currentContentIndex = 0 - n = n.right // we used the content of n, now iterate to next - } - const value = currentContent[currentContentIndex++] - // check if we need to empty currentContent - if (currentContent.length <= currentContentIndex) { - currentContent = null - } - return { - done: false, - value - } - } - } -} - /** * @todo remove / inline this * diff --git a/tests/attribution.tests.js b/tests/attribution.tests.js index 96ea0c5c..96933aa1 100644 --- a/tests/attribution.tests.js +++ b/tests/attribution.tests.js @@ -14,7 +14,7 @@ import * as delta from 'lib0/delta' */ export const testRelativePositions = _tc => { const ydoc = new Y.Doc() - const ytext = ydoc.getText() + const ytext = ydoc.get() ytext.insert(0, 'hello world') const v1 = Y.cloneDoc(ydoc) ytext.delete(1, 6) @@ -32,7 +32,7 @@ export const testRelativePositions = _tc => { */ export const testAttributedEvents = _tc => { const ydoc = new Y.Doc() - const ytext = ydoc.getText() + const ytext = ydoc.get() ytext.insert(0, 'hello world') const v1 = Y.cloneDoc(ydoc) ydoc.transact(() => { @@ -56,7 +56,7 @@ export const testAttributedEvents = _tc => { */ export const testInsertionsMindingAttributedContent = _tc => { const ydoc = new Y.Doc() - const ytext = ydoc.getText() + const ytext = ydoc.get() ytext.insert(0, 'hello world') const v1 = Y.cloneDoc(ydoc) ydoc.transact(() => { @@ -74,7 +74,7 @@ export const testInsertionsMindingAttributedContent = _tc => { */ export const testInsertionsIntoAttributedContent = _tc => { const ydoc = new Y.Doc() - const ytext = ydoc.getText() + const ytext = ydoc.get() ytext.insert(0, 'hello ') const v1 = Y.cloneDoc(ydoc) ydoc.transact(() => { @@ -89,15 +89,15 @@ export const testInsertionsIntoAttributedContent = _tc => { export const testYdocDiff = () => { const ydocStart = new Y.Doc() - ydocStart.getText('text').insert(0, 'hello') - ydocStart.getArray('array').insert(0, [1, 2, 3]) - ydocStart.getMap('map').set('k', 42) - ydocStart.getMap('map').set('nested', new Y.Array()) + ydocStart.get('text').insert(0, 'hello') + ydocStart.get('array').insert(0, [1, 2, 3]) + ydocStart.get('map').setAttr('k', 42) + ydocStart.get('map').setAttr('nested', new Y.Type()) const ydocUpdated = Y.cloneDoc(ydocStart) - ydocUpdated.getText('text').insert(5, ' world') - ydocUpdated.getArray('array').insert(1, ['x']) - ydocUpdated.getMap('map').set('newk', 42) - ydocUpdated.getMap('map').get('nested').insert(0, [1]) + ydocUpdated.get('text').insert(5, ' world') + ydocUpdated.get('array').insert(1, ['x']) + ydocUpdated.get('map').setAttr('newk', 42) + ydocUpdated.get('map').getAttr('nested').insert(0, [1]) // @todo add custom attribution const d = Y.diffDocsToDelta(ydocStart, ydocUpdated) console.log('calculated diff', d.toJSON()) @@ -111,19 +111,18 @@ export const testYdocDiff = () => { export const testChildListContent = () => { const ydocStart = new Y.Doc() const ydocUpdated = Y.cloneDoc(ydocStart) - const yf = new Y.XmlElement('test') + const yf = new Y.Type('test') let calledEvent = 0 yf.applyDelta(delta.create().insert('test content').setAttr('k', 'v')) - const yarray = ydocUpdated.getArray('array') - yarray.observeDeep((events, tr) => { + const yarray = ydocUpdated.get('array') + yarray.observeDeep(event => { calledEvent++ - const event = events.find(event => event.target === yarray) || new Y.YEvent(yarray, tr, new Set(null)) const d = event.deltaDeep const expectedD = delta.create().insert([delta.create('test').insert('test content').setAttr('k', 'v')]) t.compare(d, expectedD) }) - ydocUpdated.getArray('array').insert(0, [yf]) + ydocUpdated.get('array').insert(0, [yf]) t.assert(calledEvent === 1) const d = Y.diffDocsToDelta(ydocStart, ydocUpdated) console.log('calculated diff', d.toJSON()) diff --git a/tests/doc.tests.js b/tests/doc.tests.js index 0381a00f..8a9145f7 100644 --- a/tests/doc.tests.js +++ b/tests/doc.tests.js @@ -6,7 +6,7 @@ import * as t from 'lib0/testing' */ export const testAfterTransactionRecursion = _tc => { const ydoc = new Y.Doc() - const yxml = ydoc.getXmlFragment('') + const yxml = ydoc.get('') ydoc.on('afterTransaction', tr => { if (tr.origin === 'test') { yxml.toJSON() @@ -14,7 +14,7 @@ export const testAfterTransactionRecursion = _tc => { }) ydoc.transact(_tr => { for (let i = 0; i < 15000; i++) { - yxml.push([new Y.XmlText('a')]) + yxml.push([new Y.Type('a')]) } }, 'test') } @@ -24,12 +24,12 @@ export const testAfterTransactionRecursion = _tc => { */ export const testFindTypeInOtherDoc = _tc => { const ydoc = new Y.Doc() - const ymap = ydoc.getMap() - const ytext = ymap.set('ytext', new Y.Text()) + const ymap = ydoc.get() + const ytext = ymap.setAttr('ytext', new Y.Type()) const ydocClone = new Y.Doc() Y.applyUpdate(ydocClone, Y.encodeStateAsUpdate(ydoc)) /** - * @template {import('../src/utils/types.js').YType} Type + * @template {Y.Type} Type * @param {Type} ytype * @param {Y.Doc} otherYdoc * @return {Type} @@ -47,7 +47,7 @@ export const testFindTypeInOtherDoc = _tc => { if (rootKey == null) { throw new Error('type does not exist in other ydoc') } - return /** @type {Type} */ (otherYdoc.get(rootKey, /** @type {import('../src/utils/types.js').YTypeConstructors} */ (ytype.constructor))) + return /** @type {Type} */ (otherYdoc.get(rootKey, /** @type {import('../src/utils/ts.js').YTypeConstructors} */ (ytype.constructor))) } else { /** * If it is a sub type, we use the item id to find the history type. diff --git a/tests/snapshot.tests.js b/tests/snapshot.tests.js index 01c956f4..2ccce441 100644 --- a/tests/snapshot.tests.js +++ b/tests/snapshot.tests.js @@ -24,9 +24,9 @@ export const testBasicXmlAttributes = _tc => { yxml.setAttribute('a', '1') const snapshot2 = Y.snapshot(ydoc) yxml.setAttribute('a', '2') - t.compare(yxml.getAttributes(), { a: '2' }) - t.compare(yxml.getAttributes(snapshot2), { a: '1' }) - t.compare(yxml.getAttributes(snapshot1), {}) + t.compare(yxml.getAttrs(), { a: '2' }) + t.compare(yxml.getAttrs(snapshot2), { a: '1' }) + t.compare(yxml.getAttrs(snapshot1), {}) } /** diff --git a/tests/testHelper.js b/tests/testHelper.js index 6f8e5e1c..409481a3 100644 --- a/tests/testHelper.js +++ b/tests/testHelper.js @@ -270,7 +270,7 @@ export class TestConnector { * @param {t.TestCase} tc * @param {{users?:number}} conf * @param {InitTestObjectCallback} [initTestObject] - * @return {{testObjects:Array,testConnector:TestConnector,users:Array,array0:Y.Array,array1:Y.Array,array2:Y.Array,map0:Y.Map,map1:Y.Map,map2:Y.Map,map3:Y.Map,text0:Y.Text,text1:Y.Text,text2:Y.Text,xml0:Y.XmlElement,xml1:Y.XmlElement,xml2:Y.XmlElement}} + * @return {{testObjects:Array,testConnector:TestConnector,users:Array,array0:Y.Type,array1:Y.Type,array2:Y.Type,map0:Y.Type,map1:Y.Type,map2:Y.Type,map3:Y.Type,text0:Y.Type,text1:Y.Type,text2:Y.Type,xml0:Y.Type,xml1:Y.Type,xml2:Y.Type}} */ export const init = (tc, { users = 5 } = {}, initTestObject) => { /** @@ -293,10 +293,10 @@ export const init = (tc, { users = 5 } = {}, initTestObject) => { const y = testConnector.createY(i) y.clientID = i result.users.push(y) - result['array' + i] = y.getArray('array') - result['map' + i] = y.getMap('map') - result['xml' + i] = y.get('xml', Y.XmlElement) - result['text' + i] = y.getText('text') + result['array' + i] = y.get('array') + result['map' + i] = y.get('map') + result['xml' + i] = y.get('xml') + result['text' + i] = y.get('text') } testConnector.syncAll() result.testObjects = result.users.map(initTestObject || (() => null)) @@ -458,37 +458,37 @@ export const compare = users => { return ydoc }) users.push(.../** @type {any} */(mergedDocs)) - const userArrayValues = users.map(u => u.getArray('array').toJSON()) - const userMapValues = users.map(u => u.getMap('map').toJSON()) + const userArrayValues = users.map(u => u.get('array').toJSON().children || []) + const userMapValues = users.map(u => u.get('map').toJSON()) // @todo fix type error here // @ts-ignore const userXmlValues = users.map(u => /** @type {Y.XmlElement} */ (u.get('xml', Y.XmlElement)).toString()) - const userTextValues = users.map(u => u.getText('text').getContentDeep()) + const userTextValues = users.map(u => u.get('text').getContentDeep()) for (const u of users) { t.assert(u.store.pendingDs === null) t.assert(u.store.pendingStructs === null) } // Test Array iterator - t.compare(users[0].getArray('array').toArray(), Array.from(users[0].getArray('array'))) + t.compare(users[0].get('array').toArray(), Array.from(users[0].get('array'))) // Test Map iterator - const ymapkeys = Array.from(users[0].getMap('map').keys()) + const ymapkeys = Array.from(users[0].get('map').attrKeys()) t.assert(ymapkeys.length === Object.keys(userMapValues[0]).length) ymapkeys.forEach(key => t.assert(object.hasProperty(userMapValues[0], key))) /** * @type {Object} */ const mapRes = {} - for (const [k, v] of users[0].getMap('map')) { + for (const [k, v] of users[0].get('map')) { mapRes[k] = v instanceof Y.AbstractType ? v.toJSON() : v } t.compare(userMapValues[0], mapRes) // Compare all users for (let i = 0; i < users.length - 1; i++) { - t.compare(userArrayValues[i].length, users[i].getArray('array').length) + t.compare(userArrayValues[i].length, users[i].get('array').length) t.compare(userArrayValues[i], userArrayValues[i + 1]) t.compare(userMapValues[i], userMapValues[i + 1]) t.compare(userXmlValues[i], userXmlValues[i + 1]) - t.compare(list.toArray(userTextValues[i].children).map(a => (delta.$textOp.check(a) || delta.$insertOp.check(a)) ? a.insert.length : 0).reduce((a, b) => a + b, 0), users[i].getText('text').length) + t.compare(list.toArray(userTextValues[i].children).map(a => (delta.$textOp.check(a) || delta.$insertOp.check(a)) ? a.insert.length : 0).reduce((a, b) => a + b, 0), users[i].get('text').length) t.compare(userTextValues[i], userTextValues[i + 1], '', (_constructor, a, b) => { if (a instanceof Y.AbstractType) { t.compare(a.toJSON(), b.toJSON()) diff --git a/tests/updates.tests.js b/tests/updates.tests.js index 14c0bec5..76e0fc10 100644 --- a/tests/updates.tests.js +++ b/tests/updates.tests.js @@ -111,7 +111,7 @@ export const testMergeUpdates = tc => { compare(users) encoders.forEach(enc => { const merged = fromUpdates(users, enc) - t.compareArrays(array0.toArray(), merged.getArray('array').toArray()) + t.compareArrays(array0.toArray(), merged.get('array').toArray()) }) } @@ -184,7 +184,7 @@ const checkUpdateCases = (ydoc, updates, enc, hasDeletes) => { // enc.logUpdate(updates) const merged = new Y.Doc({ gc: false }) enc.applyUpdate(merged, mergedUpdates) - t.compareArrays(merged.getArray().toArray(), ydoc.getArray().toArray()) + t.compareArrays(merged.get().toArray(), ydoc.get().toArray()) t.compare(enc.encodeStateVector(merged), enc.encodeStateVectorFromUpdate(mergedUpdates)) if (enc.updateEventName !== 'update') { // @todo should this also work on legacy updates? for (let j = 1; j < updates.length; j++) { @@ -235,7 +235,7 @@ export const testMergeUpdates1 = _tc => { const ydoc = new Y.Doc({ gc: false }) const updates = /** @type {Array>} */ ([]) ydoc.on(enc.updateEventName, update => { updates.push(update) }) - const array = ydoc.getArray() + const array = ydoc.get() array.insert(0, [1]) array.insert(0, [2]) array.insert(0, [3]) @@ -253,7 +253,7 @@ export const testMergeUpdates2 = _tc => { const ydoc = new Y.Doc({ gc: false }) const updates = /** @type {Array>} */ ([]) ydoc.on(enc.updateEventName, update => { updates.push(update) }) - const array = ydoc.getArray() + const array = ydoc.get() array.insert(0, [1, 2]) array.delete(1, 1) array.insert(0, [3, 4]) @@ -274,7 +274,7 @@ export const testMergePendingUpdates = _tc => { yDoc.on('update', (update, _origin, _c) => { serverUpdates.splice(serverUpdates.length, 0, update) }) - const yText = yDoc.getText('textBlock') + const yText = yDoc.get('textBlock') yText.applyDelta(delta.create().insert('r')) yText.applyDelta(delta.create().insert('o')) yText.applyDelta(delta.create().insert('n')) @@ -305,7 +305,7 @@ export const testMergePendingUpdates = _tc => { Y.applyUpdate(yDoc5, serverUpdates[4]) Y.encodeStateAsUpdate(yDoc5) - const yText5 = yDoc5.getText('textBlock') + const yText5 = yDoc5.get('textBlock') t.compareStrings(yText5.toString(), 'nenor') } @@ -314,26 +314,26 @@ export const testMergePendingUpdates = _tc => { */ export const testObfuscateUpdates = _tc => { const ydoc = new Y.Doc() - const ytext = ydoc.getText('text') - const ymap = ydoc.getMap('map') - const yarray = ydoc.getArray('array') + const ytext = ydoc.get('text') + const ymap = ydoc.get('map') + const yarray = ydoc.get('array') // test ytext ytext.applyDelta(delta.create().insert('text', { bold: true }).insert([{ href: 'supersecreturl' }])) // test ymap - ymap.set('key', 'secret1') - ymap.set('key', 'secret2') + ymap.setAttr('key', 'secret1') + ymap.setAttr('key', 'secret2') // test yarray with subtype & subdoc - const subtype = new Y.XmlElement('secretnodename') + const subtype = new Y.Type('secretnodename') const subdoc = new Y.Doc({ guid: 'secret' }) - subtype.setAttribute('attr', 'val') + subtype.setAttr('attr', 'val') yarray.insert(0, ['teststring', 42, subtype, subdoc]) // obfuscate the content and put it into a new document const obfuscatedUpdate = Y.obfuscateUpdate(Y.encodeStateAsUpdate(ydoc)) const odoc = new Y.Doc() Y.applyUpdate(odoc, obfuscatedUpdate) - const otext = odoc.getText('text') - const omap = odoc.getMap('map') - const oarray = odoc.getArray('array') + const otext = odoc.get('text') + const omap = odoc.get('map') + const oarray = odoc.get('array') // test ytext const d = /** @type {any} */ (otext.getContent().toJSON().children) t.assert(d.length === 2) @@ -343,19 +343,19 @@ export const testObfuscateUpdates = _tc => { t.assert(object.length(d[1].insert) === 1) t.assert(object.hasProperty(d[1], 'insert')) // test ymap - t.assert(omap.size === 1) - t.assert(!omap.has('key')) + t.assert(omap.attrSize === 1) + t.assert(!omap.hasAttr('key')) // test yarray with subtype & subdoc const result = oarray.toArray() t.assert(result.length === 4) t.assert(result[0] !== 'teststring') t.assert(result[1] !== 42) - const osubtype = /** @type {Y.XmlElement} */ (result[2]) + const osubtype = /** @type {Y.Type} */ (result[2]) const osubdoc = result[3] // test subtype - t.assert(osubtype.nodeName !== subtype.nodeName) - t.assert(object.length(osubtype.getAttributes()) === 1) - t.assert(osubtype.getAttribute('attr') === undefined) + t.assert(osubtype.name !== subtype.name) + t.assert(object.length(osubtype.getAttrs()) === 1) + t.assert(osubtype.getAttr('attr') === undefined) // test subdoc t.assert(osubdoc.guid !== subdoc.guid) } diff --git a/tests/y-array.tests.js b/tests/y-array.tests.js index 29ccf292..0e4aa253 100644 --- a/tests/y-array.tests.js +++ b/tests/y-array.tests.js @@ -14,10 +14,10 @@ const isDevMode = env.getVariable('node_env') === 'development' export const testBasicUpdate = _tc => { const doc1 = new Y.Doc() const doc2 = new Y.Doc() - doc1.getArray('array').insert(0, ['hi']) + doc1.get('array').insert(0, ['hi']) const update = Y.encodeStateAsUpdate(doc1) Y.applyUpdate(doc2, update) - t.compare(doc2.getArray('array').toArray(), ['hi']) + t.compare(doc2.get('array').toArray(), ['hi']) } /** @@ -29,8 +29,8 @@ export const testFailsObjectManipulationInDevMode = _tc => { const doc = new Y.Doc() const a = [1, 2, 3] const b = { o: 1 } - doc.getArray('test').insert(0, [a]) - doc.getMap('map').set('k', b) + doc.get('test').insert(0, [a]) + doc.get('map').setAttr('k', b) t.fails(() => { a[0] = 42 }) @@ -47,7 +47,7 @@ export const testFailsObjectManipulationInDevMode = _tc => { */ export const testSlice = _tc => { const doc1 = new Y.Doc() - const arr = doc1.getArray('array') + const arr = doc1.get('array') arr.insert(0, [1, 2, 3]) t.compareArrays(arr.slice(0), [1, 2, 3]) t.compareArrays(arr.slice(1), [2, 3]) @@ -62,9 +62,9 @@ export const testSlice = _tc => { */ export const testArrayFrom = _tc => { const doc1 = new Y.Doc() - const db1 = doc1.getMap('root') - const nestedArray1 = Y.Array.from([0, 1, 2]) - db1.set('array', nestedArray1) + const db1 = doc1.get('root') + const nestedArray1 = Y.Type.from(delta.create().insert([0, 1, 2])) + db1.setAttr('array', nestedArray1) t.compare(nestedArray1.toArray(), [0, 1, 2]) } @@ -75,7 +75,7 @@ export const testArrayFrom = _tc => { */ export const testLengthIssue = _tc => { const doc1 = new Y.Doc() - const arr = doc1.getArray('array') + const arr = doc1.get('array') arr.push([0, 1, 2, 3]) arr.delete(0) arr.insert(0, [0]) @@ -104,7 +104,7 @@ export const testLengthIssue = _tc => { */ export const testLengthIssue2 = _tc => { const doc = new Y.Doc() - const next = doc.getArray() + const next = doc.get() doc.transact(() => { next.insert(0, ['group2']) }) @@ -162,9 +162,9 @@ export const testDeleteInsert = tc => { export const testInsertThreeElementsTryRegetProperty = tc => { const { testConnector, users, array0, array1 } = init(tc, { users: 2 }) array0.insert(0, [1, true, false]) - t.compare(array0.toJSON(), [1, true, false], '.toJSON() works') + t.compare(array0.getContent(), delta.create().insert([1, true, false]), 'content works') testConnector.flushAllMessages() - t.compare(array1.toJSON(), [1, true, false], '.toJSON() works after sync') + t.compare(array1.getContent(), delta.create().insert([1, true, false]), 'comparison works after sync') compare(users) } @@ -222,8 +222,8 @@ export const testDisconnectReallyPreventsSendingMessages = tc => { users[2].disconnect() array0.insert(1, ['user0']) array1.insert(1, ['user1']) - t.compare(array0.toJSON(), ['x', 'user0', 'y']) - t.compare(array1.toJSON(), ['x', 'user1', 'y']) + t.compare(array0.toJSON().children, ['x', 'user0', 'y']) + t.compare(array1.toJSON().children, ['x', 'user1', 'y']) users[1].connect() users[2].connect() compare(users) @@ -318,7 +318,7 @@ export const testInsertAndDeleteEventsForTypes = tc => { array0.observe(e => { event = e }) - array0.insert(0, [new Y.Array()]) + array0.insert(0, [new Y.Type()]) t.assert(event !== null) event = null array0.delete(0) @@ -327,34 +327,6 @@ export const testInsertAndDeleteEventsForTypes = tc => { compare(users) } -/** - * This issue has been reported in https://discuss.yjs.dev/t/order-in-which-events-yielded-by-observedeep-should-be-applied/261/2 - * - * Deep observers generate multiple events. When an array added at item at, say, position 0, - * and item 1 changed then the array-add event should fire first so that the change event - * path is correct. A array binding might lead to an inconsistent state otherwise. - * - * @param {t.TestCase} tc - */ -export const testObserveDeepEventOrder = tc => { - const { array0, users } = init(tc, { users: 2 }) - /** - * @type {Array} - */ - let events = [] - array0.observeDeep(e => { - events = e - }) - array0.insert(0, [new Y.Map()]) - users[0].transact(() => { - array0.get(0).set('a', 'a') - array0.insert(0, [0]) - }) - for (let i = 1; i < events.length; i++) { - t.assert(events[i - 1].path.length <= events[i].path.length, 'path size increases, fire top-level events first') - } -} - /** * Correct index when computing event.path in observeDeep - https://github.com/yjs/yjs/issues/457 * @@ -362,18 +334,18 @@ export const testObserveDeepEventOrder = tc => { */ export const testObservedeepIndexes = _tc => { const doc = new Y.Doc() - const map = doc.getMap() + const map = doc.get() // Create a field with the array as value - map.set('my-array', new Y.Array()) + map.setAttr('my-array', new Y.Type()) // Fill the array with some strings and our Map - map.get('my-array').push(['a', 'b', 'c', new Y.Map()]) + map.getAttr('my-array').push(['a', 'b', 'c', new Y.Type()]) /** * @type {Array} */ let eventPath = [] - map.observeDeep((events) => { eventPath = events[0].path }) + map.observeDeep((event) => { eventPath = event.path }) // set a value on the map inside of our array - map.get('my-array').get(3).set('hello', 'world') + map.getAttr('my-array').get(3).set('hello', 'world') console.log(eventPath) t.compare(eventPath, ['my-array', 3]) } @@ -384,13 +356,13 @@ export const testObservedeepIndexes = _tc => { export const testChangeEvent = tc => { const { array0, users } = init(tc, { users: 2 }) /** - * @type {delta.Delta} + * @type {delta.Delta} */ let d = delta.create() array0.observe(e => { d = e.delta }) - const newArr = new Y.Array() + const newArr = new Y.Type() array0.insert(0, [newArr, 4, 'dtrn']) t.assert(d !== null && d.children.len === 1) t.compare(d, delta.create().insert([newArr, 4, 'dtrn'])) @@ -415,7 +387,7 @@ export const testInsertAndDeleteEventsForTypes2 = tc => { array0.observe(e => { events.push(e) }) - array0.insert(0, ['hi', new Y.Map()]) + array0.insert(0, ['hi', new Y.Type()]) t.assert(events.length === 1, 'Event is triggered exactly once for insertion of two elements') array0.delete(1) t.assert(events.length === 2, 'Event is triggered exactly once for deletion') @@ -430,12 +402,12 @@ export const testNewChildDoesNotEmitEventInTransaction = tc => { const { array0, users } = init(tc, { users: 2 }) let fired = false users[0].transact(() => { - const newMap = new Y.Map() + const newMap = new Y.Type() newMap.observe(() => { fired = true }) array0.insert(0, [newMap]) - newMap.set('tst', 42) + newMap.setAttr('tst', 42) }) t.assert(!fired, 'Event does not trigger') } @@ -494,15 +466,15 @@ export const testEventTargetIsSetCorrectlyOnRemote = tc => { */ export const testIteratingArrayContainingTypes = _tc => { const y = new Y.Doc() - const arr = y.getArray('arr') + const arr = y.get('arr') const numItems = 10 for (let i = 0; i < numItems; i++) { - const map = new Y.Map() - map.set('value', i) + const map = new Y.Type() + map.setAttr('value', i) arr.push([map]) } let cnt = 0 - for (const item of arr) { + for (const item of arr.toArray()) { t.assert(item.get('value') === cnt++, 'value is correct') } y.destroy() @@ -514,9 +486,9 @@ export const testIteratingArrayContainingTypes = _tc => { export const testAttributedContent = _tc => { const ydoc = new Y.Doc({ gc: false }) /** - * @type {Y.Array} + * @type {Y.Type<{ children: number }>} */ - const yarray = ydoc.getArray() + const yarray = ydoc.get() yarray.insert(0, [1, 2]) let attributionManager = Y.noAttributionsManager @@ -544,7 +516,7 @@ const getUniqueNumber = () => _uniqueNumber++ */ const arrayTransactions = [ function insert (user, gen) { - const yarray = user.getArray('array') + const yarray = user.get('array') const uniqueNumber = getUniqueNumber() const content = [] const len = prng.int32(gen, 1, 4) @@ -558,35 +530,35 @@ const arrayTransactions = [ t.compareArrays(yarray.toArray(), oldContent) // we want to make sure that fastSearch markers insert at the correct position }, function insertTypeArray (user, gen) { - const yarray = user.getArray('array') + const yarray = user.get('array') const pos = prng.int32(gen, 0, yarray.length) - yarray.insert(pos, [new Y.Array()]) + yarray.insert(pos, [new Y.Type()]) const array2 = yarray.get(pos) array2.insert(0, [1, 2, 3, 4]) }, function insertTypeMap (user, gen) { - const yarray = user.getArray('array') + const yarray = user.get('array') const pos = prng.int32(gen, 0, yarray.length) - yarray.insert(pos, [new Y.Map()]) + yarray.insert(pos, [new Y.Type()]) const map = yarray.get(pos) map.set('someprop', 42) map.set('someprop', 43) map.set('someprop', 44) }, function insertTypeNull (user, gen) { - const yarray = user.getArray('array') + const yarray = user.get('array') const pos = prng.int32(gen, 0, yarray.length) yarray.insert(pos, [null]) }, function _delete (user, gen) { - const yarray = user.getArray('array') + const yarray = user.get('array') const length = yarray.length if (length > 0) { let somePos = prng.int32(gen, 0, length - 1) let delLength = prng.int32(gen, 1, math.min(2, length - somePos)) if (prng.bool(gen)) { const type = yarray.get(somePos) - if (type instanceof Y.Array && type.length > 0) { + if (type instanceof Y.Type && type.length > 0) { somePos = prng.int32(gen, 0, type.length - 1) delLength = prng.int32(gen, 0, math.min(2, type.length - somePos)) type.delete(somePos, delLength) diff --git a/tests/y-map.tests.js b/tests/y-map.tests.js index a1acc9f4..9b2fdbd6 100644 --- a/tests/y-map.tests.js +++ b/tests/y-map.tests.js @@ -1,7 +1,6 @@ import * as Y from '../src/index.js' import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line import { - compareIDs, noAttributionsManager, TwosetAttributionManager, createIdMapFromIdSet @@ -18,35 +17,34 @@ import * as object from 'lib0/object' export const testIterators = _tc => { const ydoc = new Y.Doc() /** - * @type {Y.Map} + * @type {Y.Type<{attrs: { [k:string]: number} }>} */ - const ymap = ydoc.getMap() + const ymap = ydoc.get() // we are only checking if the type assumptions are correct /** * @type {Array} */ - const vals = Array.from(ymap.values()) + const vals = Array.from(ymap.attrValues()) /** * @type {Array<[string,number]>} */ - const entries = Array.from(ymap.entries()) + const entries = Array.from(ymap.attrEntries()) /** * @type {Array} */ - const keys = Array.from(ymap.keys()) + const keys = Array.from(ymap.attrKeys()) console.log(vals, entries, keys) } export const testNestedMapEvent = () => { const ydoc = new Y.Doc() - const ymap = ydoc.getMap() - const ymapNested = ymap.set('nested', new Y.Map()) + const ymap = ydoc.get() + const ymapNested = ymap.setAttr('nested', new Y.Type()) let called = 0 - ymap.observeDeep((events, tr) => { - const event = events.find(event => event.target === ymap) || new Y.YEvent(ymap, tr, new Set()) + ymap.observeDeep(event => { const d = event.deltaDeep called++ - t.compare(d, delta.create().update('nested', delta.create().set('k', 'v'))) + t.compare(d, delta.create().modifyAttr('nested', delta.create().setAttr('k', 'v'))) }) ymapNested.set('k', 'v') t.assert(called === 1) @@ -54,17 +52,16 @@ export const testNestedMapEvent = () => { export const testNestedMapEvent2 = () => { const ydoc = new Y.Doc() - const yarr = ydoc.getArray() - const ymapNested = new Y.Map() + const yarr = ydoc.get() + const ymapNested = new Y.Type() yarr.insert(0, [ymapNested]) let called = 0 - yarr.observeDeep((events, tr) => { - const event = events.find(event => event.target === yarr) || new Y.YEvent(yarr, tr, new Set()) + yarr.observeDeep(event => { const d = event.deltaDeep called++ - t.compare(d, delta.create().modify(delta.create().set('k', 'v'))) + t.compare(d, delta.create().modify(delta.create().setAttr('k', 'v'))) }) - ymapNested.set('k', 'v') + ymapNested.setAttr('k', 'v') t.assert(called === 1) } @@ -75,7 +72,7 @@ export const testNestedMapEvent2 = () => { */ export const testMapEventError = _tc => { const doc = new Y.Doc() - const ymap = doc.getMap() + const ymap = doc.get() /** * @type {any} */ @@ -96,26 +93,20 @@ export const testMapEventError = _tc => { */ export const testMapHavingIterableAsConstructorParamTests = tc => { const { map0 } = init(tc, { users: 1 }) - - const m1 = new Y.Map(Object.entries({ number: 1, string: 'hello' })) - map0.set('m1', m1) - t.assert(m1.get('number') === 1) - t.assert(m1.get('string') === 'hello') - - const m2 = new Y.Map([ - ['object', { x: 1 }], - ['boolean', true] - ]) - map0.set('m2', m2) - t.assert(m2.get('object').x === 1) - t.assert(m2.get('boolean') === true) - - const m3 = new Y.Map([...m1, ...m2]) - map0.set('m3', m3) - t.assert(m3.get('number') === 1) - t.assert(m3.get('string') === 'hello') - t.assert(m3.get('object').x === 1) - t.assert(m3.get('boolean') === true) + const m1 = Y.Type.from(delta.create().setAttr('number', 1).setAttr('string', 'hello')) + map0.setAttr('m1', m1) + t.assert(m1.getAttr('number') === 1) + t.assert(m1.getAttr('string') === 'hello') + const m2 = Y.Type.from(delta.create(delta.$deltaAny).setAttrs({ object: { x: 1 }, boolean: true }).done()) + map0.setAttr('m2', m2) + t.assert(m2.getAttr('object')?.x === 1) + t.assert(m2.getAttr('boolean') === true) + const m3 = new Y.Type().applyDelta(m1.getContent()).applyDelta(m2.getContent()) + map0.setAttr('m3', m3) + t.assert(m3.getAttr('number') === 1) + t.assert(m3.getAttr('string') === 'hello') + t.assert(m3.getAttr('object')?.x === 1) + t.assert(m3.getAttr('boolean') === true) } /** @@ -125,48 +116,48 @@ export const testBasicMapTests = tc => { const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 }) users[2].disconnect() - map0.set('null', null) - map0.set('number', 1) - map0.set('string', 'hello Y') - map0.set('object', { key: { key2: 'value' } }) - map0.set('y-map', new Y.Map()) - map0.set('boolean1', true) - map0.set('boolean0', false) - const map = map0.get('y-map') - map.set('y-array', new Y.Array()) + map0.setAttr('null', null) + map0.setAttr('number', 1) + map0.setAttr('string', 'hello Y') + map0.setAttr('object', { key: { key2: 'value' } }) + map0.setAttr('y-map', new Y.Type()) + map0.setAttr('boolean1', true) + map0.setAttr('boolean0', false) + const map = map0.getAttr('y-map') + map.set('y-array', new Y.Type()) const array = map.get('y-array') array.insert(0, [0]) array.insert(0, [-1]) - t.assert(map0.get('null') === null, 'client 0 computed the change (null)') - t.assert(map0.get('number') === 1, 'client 0 computed the change (number)') - t.assert(map0.get('string') === 'hello Y', 'client 0 computed the change (string)') - t.assert(map0.get('boolean0') === false, 'client 0 computed the change (boolean)') - t.assert(map0.get('boolean1') === true, 'client 0 computed the change (boolean)') - t.compare(map0.get('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)') - t.assert(map0.get('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)') - t.assert(map0.size === 7, 'client 0 map has correct size') + t.assert(map0.getAttr('null') === null, 'client 0 computed the change (null)') + t.assert(map0.getAttr('number') === 1, 'client 0 computed the change (number)') + t.assert(map0.getAttr('string') === 'hello Y', 'client 0 computed the change (string)') + t.assert(map0.getAttr('boolean0') === false, 'client 0 computed the change (boolean)') + t.assert(map0.getAttr('boolean1') === true, 'client 0 computed the change (boolean)') + t.compare(map0.getAttr('object'), { key: { key2: 'value' } }, 'client 0 computed the change (object)') + t.assert(map0.getAttr('y-map').get('y-array').get(0) === -1, 'client 0 computed the change (type)') + t.assert(map0.attrSize === 7, 'client 0 map has correct size') users[2].connect() testConnector.flushAllMessages() - t.assert(map1.get('null') === null, 'client 1 received the update (null)') - t.assert(map1.get('number') === 1, 'client 1 received the update (number)') - t.assert(map1.get('string') === 'hello Y', 'client 1 received the update (string)') - t.assert(map1.get('boolean0') === false, 'client 1 computed the change (boolean)') - t.assert(map1.get('boolean1') === true, 'client 1 computed the change (boolean)') - t.compare(map1.get('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)') - t.assert(map1.get('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)') - t.assert(map1.size === 7, 'client 1 map has correct size') + t.assert(map1.getAttr('null') === null, 'client 1 received the update (null)') + t.assert(map1.getAttr('number') === 1, 'client 1 received the update (number)') + t.assert(map1.getAttr('string') === 'hello Y', 'client 1 received the update (string)') + t.assert(map1.getAttr('boolean0') === false, 'client 1 computed the change (boolean)') + t.assert(map1.getAttr('boolean1') === true, 'client 1 computed the change (boolean)') + t.compare(map1.getAttr('object'), { key: { key2: 'value' } }, 'client 1 received the update (object)') + t.assert(map1.getAttr('y-map').get('y-array').get(0) === -1, 'client 1 received the update (type)') + t.assert(map1.attrSize === 7, 'client 1 map has correct size') // compare disconnected user - t.assert(map2.get('null') === null, 'client 2 received the update (null) - was disconnected') - t.assert(map2.get('number') === 1, 'client 2 received the update (number) - was disconnected') - t.assert(map2.get('string') === 'hello Y', 'client 2 received the update (string) - was disconnected') - t.assert(map2.get('boolean0') === false, 'client 2 computed the change (boolean)') - t.assert(map2.get('boolean1') === true, 'client 2 computed the change (boolean)') - t.compare(map2.get('object'), { key: { key2: 'value' } }, 'client 2 received the update (object) - was disconnected') - t.assert(map2.get('y-map').get('y-array').get(0) === -1, 'client 2 received the update (type) - was disconnected') + t.assert(map2.getAttr('null') === null, 'client 2 received the update (null) - was disconnected') + t.assert(map2.getAttr('number') === 1, 'client 2 received the update (number) - was disconnected') + t.assert(map2.getAttr('string') === 'hello Y', 'client 2 received the update (string) - was disconnected') + t.assert(map2.getAttr('boolean0') === false, 'client 2 computed the change (boolean)') + t.assert(map2.getAttr('boolean1') === true, 'client 2 computed the change (boolean)') + t.compare(map2.getAttr('object'), { key: { key2: 'value' } }, 'client 2 received the update (object) - was disconnected') + t.assert(map2.getAttr('y-map').get('y-array').get(0) === -1, 'client 2 received the update (type) - was disconnected') compare(users) } @@ -175,18 +166,18 @@ export const testBasicMapTests = tc => { */ export const testGetAndSetOfMapProperty = tc => { const { testConnector, users, map0 } = init(tc, { users: 2 }) - map0.set('stuff', 'stuffy') - map0.set('undefined', undefined) - map0.set('null', null) - t.compare(map0.get('stuff'), 'stuffy') + map0.setAttr('stuff', 'stuffy') + map0.setAttr('undefined', undefined) + map0.setAttr('null', null) + t.compare(map0.getAttr('stuff'), 'stuffy') testConnector.flushAllMessages() for (const user of users) { - const u = user.getMap('map') - t.compare(u.get('stuff'), 'stuffy') - t.assert(u.get('undefined') === undefined, 'undefined') - t.compare(u.get('null'), null, 'null') + const u = user.get('map') + t.compare(u.getAttr('stuff'), 'stuffy') + t.assert(u.getAttr('undefined') === undefined, 'undefined') + t.compare(u.getAttr('null'), null, 'null') } compare(users) } @@ -196,8 +187,8 @@ export const testGetAndSetOfMapProperty = tc => { */ export const testYmapSetsYmap = tc => { const { users, map0 } = init(tc, { users: 2 }) - const map = map0.set('Map', new Y.Map()) - t.assert(map0.get('Map') === map) + const map = map0.setAttr('Map', new Y.Type()) + t.assert(map0.getAttr('Map') === map) map.set('one', 1) t.compare(map.get('one'), 1) compare(users) @@ -208,8 +199,8 @@ export const testYmapSetsYmap = tc => { */ export const testYmapSetsYarray = tc => { const { users, map0 } = init(tc, { users: 2 }) - const array = map0.set('Array', new Y.Array()) - t.assert(array === map0.get('Array')) + const array = map0.setAttr('Array', new Y.Type()) + t.assert(array === map0.getAttr('Array')) array.insert(0, [1, 2, 3]) // @ts-ignore t.compare(map0.toJSON(), { Array: [1, 2, 3] }) @@ -221,12 +212,12 @@ export const testYmapSetsYarray = tc => { */ export const testGetAndSetOfMapPropertySyncs = tc => { const { testConnector, users, map0 } = init(tc, { users: 2 }) - map0.set('stuff', 'stuffy') - t.compare(map0.get('stuff'), 'stuffy') + map0.setAttr('stuff', 'stuffy') + t.compare(map0.getAttr('stuff'), 'stuffy') testConnector.flushAllMessages() for (const user of users) { - const u = user.getMap('map') - t.compare(u.get('stuff'), 'stuffy') + const u = user.get('map') + t.compare(u.getAttr('stuff'), 'stuffy') } compare(users) } @@ -236,12 +227,12 @@ export const testGetAndSetOfMapPropertySyncs = tc => { */ export const testGetAndSetOfMapPropertyWithConflict = tc => { const { testConnector, users, map0, map1 } = init(tc, { users: 3 }) - map0.set('stuff', 'c0') - map1.set('stuff', 'c1') + map0.setAttr('stuff', 'c0') + map1.setAttr('stuff', 'c1') testConnector.flushAllMessages() for (const user of users) { - const u = user.getMap('map') - t.compare(u.get('stuff'), 'c1') + const u = user.get('map') + t.compare(u.getAttr('stuff'), 'c1') } compare(users) } @@ -251,13 +242,13 @@ export const testGetAndSetOfMapPropertyWithConflict = tc => { */ export const testSizeAndDeleteOfMapProperty = tc => { const { map0 } = init(tc, { users: 1 }) - map0.set('stuff', 'c0') - map0.set('otherstuff', 'c1') - t.assert(map0.size === 2, `map size is ${map0.size} expected 2`) - map0.delete('stuff') - t.assert(map0.size === 1, `map size after delete is ${map0.size}, expected 1`) - map0.delete('otherstuff') - t.assert(map0.size === 0, `map size after delete is ${map0.size}, expected 0`) + map0.setAttr('stuff', 'c0') + map0.setAttr('otherstuff', 'c1') + t.assert(map0.attrSize === 2, `map size is ${map0.attrSize} expected 2`) + map0.deleteAttr('stuff') + t.assert(map0.attrSize === 1, `map size after delete is ${map0.attrSize}, expected 1`) + map0.deleteAttr('otherstuff') + t.assert(map0.attrSize === 0, `map size after delete is ${map0.attrSize}, expected 0`) } /** @@ -265,13 +256,13 @@ export const testSizeAndDeleteOfMapProperty = tc => { */ export const testGetAndSetAndDeleteOfMapProperty = tc => { const { testConnector, users, map0, map1 } = init(tc, { users: 3 }) - map0.set('stuff', 'c0') - map1.set('stuff', 'c1') - map1.delete('stuff') + map0.setAttr('stuff', 'c0') + map1.setAttr('stuff', 'c1') + map1.deleteAttr('stuff') testConnector.flushAllMessages() for (const user of users) { - const u = user.getMap('map') - t.assert(u.get('stuff') === undefined) + const u = user.get('map') + t.assert(u.getAttr('stuff') === undefined) } compare(users) } @@ -281,15 +272,15 @@ export const testGetAndSetAndDeleteOfMapProperty = tc => { */ export const testSetAndClearOfMapProperties = tc => { const { testConnector, users, map0 } = init(tc, { users: 1 }) - map0.set('stuff', 'c0') - map0.set('otherstuff', 'c1') - map0.clear() + map0.setAttr('stuff', 'c0') + map0.setAttr('otherstuff', 'c1') + map0.clearAttrs() testConnector.flushAllMessages() for (const user of users) { - const u = user.getMap('map') - t.assert(u.get('stuff') === undefined) - t.assert(u.get('otherstuff') === undefined) - t.assert(u.size === 0, `map size after clear is ${u.size}, expected 0`) + const u = user.get('map') + t.assert(u.getAttr('stuff') === undefined) + t.assert(u.getAttr('otherstuff') === undefined) + t.assert(u.attrSize === 0, `map size after clear is ${u.attrSize}, expected 0`) } compare(users) } @@ -299,22 +290,22 @@ export const testSetAndClearOfMapProperties = tc => { */ export const testSetAndClearOfMapPropertiesWithConflicts = tc => { const { testConnector, users, map0, map1, map2, map3 } = init(tc, { users: 4 }) - map0.set('stuff', 'c0') - map1.set('stuff', 'c1') - map1.set('stuff', 'c2') - map2.set('stuff', 'c3') + map0.setAttr('stuff', 'c0') + map1.setAttr('stuff', 'c1') + map1.setAttr('stuff', 'c2') + map2.setAttr('stuff', 'c3') testConnector.flushAllMessages() - map0.set('otherstuff', 'c0') - map1.set('otherstuff', 'c1') - map2.set('otherstuff', 'c2') - map3.set('otherstuff', 'c3') - map3.clear() + map0.setAttr('otherstuff', 'c0') + map1.setAttr('otherstuff', 'c1') + map2.setAttr('otherstuff', 'c2') + map3.setAttr('otherstuff', 'c3') + map3.clearAttrs() testConnector.flushAllMessages() for (const user of users) { - const u = user.getMap('map') - t.assert(u.get('stuff') === undefined) - t.assert(u.get('otherstuff') === undefined) - t.assert(u.size === 0, `map size after clear is ${u.size}, expected 0`) + const u = user.get('map') + t.assert(u.getAttr('stuff') === undefined) + t.assert(u.getAttr('otherstuff') === undefined) + t.assert(u.attrSize === 0, `map size after clear is ${u.attrSize}, expected 0`) } compare(users) } @@ -324,14 +315,14 @@ export const testSetAndClearOfMapPropertiesWithConflicts = tc => { */ export const testGetAndSetOfMapPropertyWithThreeConflicts = tc => { const { testConnector, users, map0, map1, map2 } = init(tc, { users: 3 }) - map0.set('stuff', 'c0') - map1.set('stuff', 'c1') - map1.set('stuff', 'c2') - map2.set('stuff', 'c3') + map0.setAttr('stuff', 'c0') + map1.setAttr('stuff', 'c1') + map1.setAttr('stuff', 'c2') + map2.setAttr('stuff', 'c3') testConnector.flushAllMessages() for (const user of users) { - const u = user.getMap('map') - t.compare(u.get('stuff'), 'c3') + const u = user.get('map') + t.compare(u.getAttr('stuff'), 'c3') } compare(users) } @@ -341,114 +332,24 @@ export const testGetAndSetOfMapPropertyWithThreeConflicts = tc => { */ export const testGetAndSetAndDeleteOfMapPropertyWithThreeConflicts = tc => { const { testConnector, users, map0, map1, map2, map3 } = init(tc, { users: 4 }) - map0.set('stuff', 'c0') - map1.set('stuff', 'c1') - map1.set('stuff', 'c2') - map2.set('stuff', 'c3') + map0.setAttr('stuff', 'c0') + map1.setAttr('stuff', 'c1') + map1.setAttr('stuff', 'c2') + map2.setAttr('stuff', 'c3') testConnector.flushAllMessages() - map0.set('stuff', 'deleteme') - map1.set('stuff', 'c1') - map2.set('stuff', 'c2') - map3.set('stuff', 'c3') - map3.delete('stuff') + map0.setAttr('stuff', 'deleteme') + map1.setAttr('stuff', 'c1') + map2.setAttr('stuff', 'c2') + map3.setAttr('stuff', 'c3') + map3.deleteAttr('stuff') testConnector.flushAllMessages() for (const user of users) { - const u = user.getMap('map') - t.assert(u.get('stuff') === undefined) + const u = user.get('map') + t.assert(u.getAttr('stuff') === undefined) } compare(users) } -/** - * @param {t.TestCase} tc - */ -export const testObserveDeepProperties = tc => { - const { testConnector, users, map1, map2, map3 } = init(tc, { users: 4 }) - const _map1 = map1.set('map', new Y.Map()) - let calls = 0 - let dmapid - map1.observeDeep(events => { - events.forEach(event => { - calls++ - // @ts-ignore - t.assert(event.keysChanged.has('deepmap')) - t.assert(event.path.length === 1) - t.assert(event.path[0] === 'map') - // @ts-ignore - dmapid = event.target.get('deepmap')._item.id - }) - }) - testConnector.flushAllMessages() - const _map3 = map3.get('map') - _map3.set('deepmap', new Y.Map()) - testConnector.flushAllMessages() - const _map2 = map2.get('map') - _map2.set('deepmap', new Y.Map()) - testConnector.flushAllMessages() - const dmap1 = _map1.get('deepmap') - const dmap2 = _map2.get('deepmap') - const dmap3 = _map3.get('deepmap') - t.assert(calls > 0) - t.assert(compareIDs(dmap1._item.id, dmap2._item.id)) - t.assert(compareIDs(dmap1._item.id, dmap3._item.id)) - // @ts-ignore we want the possibility of dmapid being undefined - t.assert(compareIDs(dmap1._item.id, dmapid)) - compare(users) -} - -/** - * @param {t.TestCase} tc - */ -export const testObserversUsingObservedeep = tc => { - const { users, map0 } = init(tc, { users: 2 }) - /** - * @type {Array>} - */ - const paths = [] - let calls = 0 - map0.observeDeep(events => { - events.forEach(event => { - paths.push(event.path) - }) - calls++ - }) - map0.set('map', new Y.Map()) - map0.get('map').set('array', new Y.Array()) - map0.get('map').get('array').insert(0, ['content']) - t.assert(calls === 3) - t.compare(paths, [[], ['map'], ['map', 'array']]) - compare(users) -} - -/** - * @param {t.TestCase} tc - */ -export const testPathsOfSiblingEvents = tc => { - const { users, map0 } = init(tc, { users: 2 }) - /** - * @type {Array>} - */ - const paths = [] - let calls = 0 - const doc = users[0] - map0.set('map', new Y.Map()) - map0.get('map').set('text1', new Y.Text('initial')) - map0.observeDeep(events => { - events.forEach(event => { - paths.push(event.path) - }) - calls++ - }) - doc.transact(() => { - map0.get('map').get('text1').insert(0, 'post-') - map0.get('map').set('text2', new Y.Text('new')) - }) - t.assert(calls === 1) - t.compare(paths, [['map'], ['map', 'text1']]) - compare(users) -} - -// TODO: Test events in Y.Map /** * @param {Object} is * @param {Object} should @@ -471,21 +372,21 @@ export const testThrowsAddAndUpdateAndDeleteEvents = tc => { map0.observe(e => { event = e // just put it on event, should be thrown synchronously anyway }) - map0.set('stuff', 4) + map0.setAttr('stuff', 4) compareEvent(event, { target: map0, keysChanged: new Set(['stuff']) }) // update, oldValue is in contents - map0.set('stuff', new Y.Array()) + map0.setAttr('stuff', new Y.Type()) compareEvent(event, { target: map0, keysChanged: new Set(['stuff']) }) // update, oldValue is in opContents - map0.set('stuff', 5) + map0.setAttr('stuff', 5) // delete - map0.delete('stuff') + map0.deleteAttr('stuff') compareEvent(event, { keysChanged: new Set(['stuff']), target: map0 @@ -506,10 +407,10 @@ export const testThrowsDeleteEventsOnClear = tc => { event = e // just put it on event, should be thrown synchronously anyway }) // set values - map0.set('stuff', 4) - map0.set('otherstuff', new Y.Array()) + map0.setAttr('stuff', 4) + map0.setAttr('otherstuff', new Y.Type()) // clear - map0.clear() + map0.clearAttrs() compareEvent(event, { keysChanged: new Set(['stuff', 'otherstuff']), target: map0 @@ -523,38 +424,38 @@ export const testThrowsDeleteEventsOnClear = tc => { export const testChangeEvent = tc => { const { map0, users } = init(tc, { users: 2 }) /** - * @type {delta.Delta?} + * @type {delta.Delta?} */ let changes = delta.create() map0.observe(e => { changes = e.delta }) - map0.set('a', 1) + map0.setAttr('a', 1) let keyChange = changes.attrs.a t.assert(delta.$insertOpWith(s.$number).check(keyChange) && keyChange.prevValue === undefined) - map0.set('a', 2) + map0.setAttr('a', 2) keyChange = changes.attrs.a t.assert(delta.$insertOpWith(s.$number).check(keyChange) && keyChange.prevValue === 1) users[0].transact(() => { - map0.set('a', 3) - map0.set('a', 4) + map0.setAttr('a', 3) + map0.setAttr('a', 4) }) keyChange = changes.attrs.a t.assert(delta.$insertOpWith(s.$number).check(keyChange) && keyChange.prevValue === 2) users[0].transact(() => { - map0.set('b', 1) - map0.set('b', 2) + map0.setAttr('b', 1) + map0.setAttr('b', 2) }) keyChange = changes.attrs.b t.assert(delta.$insertOpWith(s.$number).check(keyChange) && keyChange.prevValue === undefined) users[0].transact(() => { - map0.set('c', 1) - map0.delete('c') + map0.setAttr('c', 1) + map0.deleteAttr('c') }) t.assert(changes !== null && object.isEmpty(changes.attrs)) users[0].transact(() => { - map0.set('d', 1) - map0.set('d', 2) + map0.setAttr('d', 1) + map0.setAttr('d', 2) }) keyChange = changes.attrs.d t.assert(delta.$insertOpWith(s.$number).check(keyChange) && keyChange.prevValue === undefined) @@ -566,7 +467,7 @@ export const testChangeEvent = tc => { */ export const testYmapEventExceptionsShouldCompleteTransaction = _tc => { const doc = new Y.Doc() - const map = doc.getMap('map') + const map = doc.get('map') let updateCalled = false let throwingObserverCalled = false @@ -589,7 +490,7 @@ export const testYmapEventExceptionsShouldCompleteTransaction = _tc => { map.observeDeep(throwingDeepObserver) t.fails(() => { - map.set('y', '2') + map.setAttr('y', '2') }) t.assert(updateCalled) @@ -601,14 +502,14 @@ export const testYmapEventExceptionsShouldCompleteTransaction = _tc => { throwingObserverCalled = false throwingDeepObserverCalled = false t.fails(() => { - map.set('z', '3') + map.setAttr('z', '3') }) t.assert(updateCalled) t.assert(throwingObserverCalled) t.assert(throwingDeepObserverCalled) - t.assert(map.get('z') === '3') + t.assert(map.getAttr('z') === '3') } /** @@ -623,7 +524,7 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitive = tc => { map0.observe(e => { event = e }) - map0.set('stuff', 2) + map0.setAttr('stuff', 2) t.compare(event.value, event.target.get(event.name)) compare(users) } @@ -640,7 +541,7 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc map0.observe(e => { event = e }) - map1.set('stuff', 2) + map1.setAttr('stuff', 2) testConnector.flushAllMessages() t.compare(event.value, event.target.get(event.name)) compare(users) @@ -651,7 +552,7 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc */ export const testAttributedContent = _tc => { const ydoc = new Y.Doc({ gc: false }) - const ymap = ydoc.getMap() + const ymap = ydoc.get() let attributionManager = noAttributionsManager ydoc.on('afterTransaction', tr => { @@ -659,21 +560,21 @@ export const testAttributedContent = _tc => { attributionManager = new TwosetAttributionManager(createIdMapFromIdSet(tr.insertSet, []), createIdMapFromIdSet(tr.deleteSet, [])) }) t.group('initial value', () => { - ymap.set('test', 42) + ymap.setAttr('test', 42) const expectedContent = { test: delta.$deltaMapChangeJson.expect({ type: 'insert', value: 42, attribution: { insert: [] } }) } const attributedContent = ymap.getContent(attributionManager) console.log(attributedContent.toJSON()) t.compare(expectedContent, attributedContent.toJSON().attrs) }) t.group('overwrite value', () => { - ymap.set('test', 'fourtytwo') + ymap.setAttr('test', 'fourtytwo') const expectedContent = { test: delta.$deltaMapChangeJson.expect({ type: 'insert', value: 'fourtytwo', attribution: { insert: [] } }) } const attributedContent = ymap.getContent(attributionManager) console.log(attributedContent) t.compare(expectedContent, attributedContent.toJSON().attrs) }) t.group('delete value', () => { - ymap.delete('test') + ymap.deleteAttr('test') const expectedContent = { test: delta.$deltaMapChangeJson.expect({ type: 'delete', prevValue: 'fourtytwo', attribution: { delete: [] } }) } const attributedContent = ymap.getContent(attributionManager) console.log(attributedContent.toJSON()) @@ -688,21 +589,21 @@ const mapTransactions = [ function set (user, gen) { const key = prng.oneOf(gen, ['one', 'two']) const value = prng.utf16String(gen) - user.getMap('map').set(key, value) + user.get('map').setAttr(key, value) }, function setType (user, gen) { const key = prng.oneOf(gen, ['one', 'two']) - const type = prng.oneOf(gen, [new Y.Array(), new Y.Map()]) - user.getMap('map').set(key, type) - if (type instanceof Y.Array) { + const type = new Y.Type() + user.get('map').setAttr(key, type) + if (prng.bool(gen)) { type.insert(0, [1, 2, 3, 4]) } else { - type.set('deepkey', 'deepvalue') + type.setAttr('deepkey', 'deepvalue') } }, function _delete (user, gen) { const key = prng.oneOf(gen, ['one', 'two']) - user.getMap('map').delete(key) + user.get('map').deleteAttr(key) } ] diff --git a/tests/y-text.tests.js b/tests/y-text.tests.js index 1a9fbcd6..250eff12 100644 --- a/tests/y-text.tests.js +++ b/tests/y-text.tests.js @@ -148,7 +148,7 @@ export const testDeltaBug = _tc => { 'block-id': 'block-15630542-ef45-412d-9415-88f0052238ce' }) const ydoc1 = new Y.Doc() - const ytext = ydoc1.getText() + const ytext = ydoc1.get() ytext.applyDelta(initialDelta) const addingDash = delta.create().retain(12).insert('-') ytext.applyDelta(addingDash) @@ -168,7 +168,7 @@ export const testDeltaBug = _tc => { }) ytext.applyDelta(addingList) const result = ytext.getContent() - const expectedResult = delta.text() + const expectedResult = delta.create() .insert('\n', { 'block-id': 'block-28eea923-9cbb-4b6f-a950-cf7fd82bc087' }) .insert('\n\n\n', { 'table-col': { width: '150' } }) .insert('\n', { @@ -306,7 +306,7 @@ export const testDeltaBug = _tc => { * @param {t.TestCase} _tc */ export const testDeltaBug2 = _tc => { - const initialContent = delta.create() + const initialContent = delta.create(delta.$deltaAny) .insert("Thomas' section") .insert('\n', { 'block-id': 'block-61ae80ac-a469-4eae-bac9-3b6a2c380118' }) .insert('\n', { 'block-id': 'block-d265d93f-1cc7-40ee-bb58-8270fca2619f' }) @@ -1206,7 +1206,7 @@ export const testDeltaBug2 = _tc => { }) .insert('\n', { 'block-id': 'block-21099df0-afb2-4cd3-834d-bb37800eb06a' }) const ydoc = new Y.Doc() - const ytext = ydoc.getText('id') + const ytext = ydoc.get('id') ytext.applyDelta(initialContent) const changeEvent = delta.create().retain(90).delete(4).retain(1, { layout: null, @@ -1350,7 +1350,7 @@ export const testFalsyFormats = tc => { */ export const testMultilineFormat = _tc => { const ydoc = new Y.Doc() - const testText = ydoc.getText('test') + const testText = ydoc.get('test') testText.insert(0, 'Test\nMulti-line\nFormatting') const tt = delta.create() .retain(4, { bold: true }) @@ -1376,7 +1376,7 @@ export const testMultilineFormat = _tc => { */ export const testNotMergeEmptyLinesFormat = _tc => { const ydoc = new Y.Doc() - const testText = ydoc.getText('test') + const testText = ydoc.get('test') testText.applyDelta(delta.create() .insert('Text') .insert('\n', { title: true }) @@ -1399,7 +1399,7 @@ export const testNotMergeEmptyLinesFormat = _tc => { */ export const testPreserveAttributesThroughDelete = _tc => { const ydoc = new Y.Doc() - const testText = ydoc.getText('test') + const testText = ydoc.get('test') testText.applyDelta(delta.create() .insert('Text') .insert('\n', { title: true }) @@ -1437,7 +1437,7 @@ export const testGetDeltaWithEmbeds = tc => { export const testTypesAsEmbed = tc => { const { text0, text1, testConnector } = init(tc, { users: 2 }) text0.applyDelta(delta.create() - .insert([new Y.Map([['key', 'val']])]) + .insert([Y.Type.from(delta.create().setAttr('key', 'val'))]) ) t.compare(/** @type {any} */ (text0).getContentDeep().toJSON().children, [{ type: 'insert', insert: [{ type: 'delta', attrs: { key: { type: 'insert', value: 'val' } } }] }]) let firedEvent = false @@ -1512,10 +1512,10 @@ export const testSnapshotDeleteAfter = tc => { /** * @param {t.TestCase} tc */ -export const testToJson = tc => { +export const testDeltaCompare = tc => { const { text0 } = init(tc, { users: 1 }) text0.insert(0, 'abc', { bold: true }) - t.assert(text0.toJSON() === 'abc', 'toJSON returns the unformatted text') + t.compare(text0.getContent(), delta.create().insert('abc', { bold: true }).done()) } /** @@ -1524,7 +1524,7 @@ export const testToJson = tc => { export const testToDeltaEmbedAttributes = tc => { const { text0 } = init(tc, { users: 1 }) text0.insert(0, 'ab', { bold: true }) - text0.insertEmbed(1, { image: 'imageSrc.png' }, { width: 100 }) + text0.insert(1, [{ image: 'imageSrc.png' }], { width: 100 }) const delta0 = text0.getContent() t.compare( delta0, @@ -1542,7 +1542,7 @@ export const testToDeltaEmbedAttributes = tc => { export const testToDeltaEmbedNoAttributes = tc => { const { text0 } = init(tc, { users: 1 }) text0.insert(0, 'ab', { bold: true }) - text0.insertEmbed(1, { image: 'imageSrc.png' }) + text0.insert(1, [{ image: 'imageSrc.png' }]) const delta0 = text0.getContent() t.compare( delta0, @@ -1593,7 +1593,7 @@ export const testFormattingDeltaUnnecessaryAttributeChange = tc => { }) testConnector.flushAllMessages() /** - * @type {Array>} + * @type {Array>} */ const deltas = [] text0.observe(event => { @@ -1706,7 +1706,7 @@ export const testLargeFragmentedDocument = _tc => { let update = /** @type {any} */ (null) ;(() => { const doc1 = new Y.Doc() - const text0 = doc1.getText('txt') + const text0 = doc1.get('txt') tryGc() t.measureTime(`time to insert ${itemsToInsert} items`, () => { doc1.transact(() => { @@ -1741,7 +1741,7 @@ export const testIncrementalUpdatesPerformanceOnLargeFragmentedDocument = _tc => doc1.on('update', update => { updates.push(update) }) - const text0 = doc1.getText('txt') + const text0 = doc1.get('txt') tryGc() t.measureTime(`time to insert ${itemsToInsert} items`, () => { doc1.transact(() => { @@ -1846,13 +1846,13 @@ export const testSearchMarkerBug1 = tc => { export const testFormattingBug = async _tc => { const ydoc1 = new Y.Doc() const ydoc2 = new Y.Doc() - const text1 = ydoc1.getText() + const text1 = ydoc1.get() text1.insert(0, '\n\n\n') text1.format(0, 3, { url: 'http://example.com' }) - ydoc1.getText().format(1, 1, { url: 'http://docs.yjs.dev' }) - ydoc2.getText().format(1, 1, { url: 'http://docs.yjs.dev' }) + ydoc1.get().format(1, 1, { url: 'http://docs.yjs.dev' }) + ydoc2.get().format(1, 1, { url: 'http://docs.yjs.dev' }) Y.applyUpdate(ydoc2, Y.encodeStateAsUpdate(ydoc1)) - const text2 = ydoc2.getText() + const text2 = ydoc2.get() const expectedResult = delta.create() .insert('\n', { url: 'http://example.com' }) .insert('\n', { url: 'http://docs.yjs.dev' }) @@ -1870,11 +1870,11 @@ export const testFormattingBug = async _tc => { */ export const testDeleteFormatting = _tc => { const doc = new Y.Doc() - const text = doc.getText() + const text = doc.get() text.insert(0, 'Attack ships on fire off the shoulder of Orion.') const doc2 = new Y.Doc() - const text2 = doc2.getText() + const text2 = doc2.get() Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc)) text.format(13, 7, { bold: true }) @@ -1897,7 +1897,7 @@ export const testDeleteFormatting = _tc => { */ export const testAttributedContent = _tc => { const ydoc = new Y.Doc({ gc: false }) - const ytext = ydoc.getText() + const ytext = ydoc.get() ytext.insert(0, 'Hello World!') let attributionManager = noAttributionsManager @@ -1927,11 +1927,11 @@ export const testAttributedContent = _tc => { export const testAttributedDiffing = _tc => { const ydocVersion0 = new Y.Doc({ gc: false }) ydocVersion0.clientID = 0 - ydocVersion0.getText().insert(0, 'Hello World!') + ydocVersion0.get().insert(0, 'Hello World!') const ydoc = new Y.Doc({ gc: false }) ydoc.clientID = 1 Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(ydocVersion0)) - const ytext = ydoc.getText() + const ytext = ydoc.get() ytext.applyDelta(delta.create().retain(4, { italic: true }).retain(2).delete(5).insert('attributions')) // this represents to all insertions of ydoc const insertionSet = Y.createInsertionSetFromStructStore(ydoc.store, false) @@ -1968,7 +1968,7 @@ const textChanges = [ * @param {prng.PRNG} gen */ (y, gen) => { // insert text - const ytext = y.getText('text') + const ytext = y.get('text') const insertPos = prng.int32(gen, 0, ytext.length) const text = charCounter++ + prng.word(gen) const prevText = ytext.toString() @@ -1980,7 +1980,7 @@ const textChanges = [ * @param {prng.PRNG} gen */ (y, gen) => { // delete text - const ytext = y.getText('text') + const ytext = y.get('text') const contentLen = ytext.toString().length const insertPos = prng.int32(gen, 0, contentLen) const overwrite = math.min(prng.int32(gen, 0, contentLen - insertPos), 2) @@ -2075,7 +2075,7 @@ const qChanges = [ * @param {prng.PRNG} gen */ (y, gen) => { // insert text - const ytext = y.getText('text') + const ytext = y.get('text') const insertPos = prng.int32(gen, 0, ytext.length) const attrs = prng.oneOf(gen, marksChoices) const text = charCounter++ + prng.word(gen) @@ -2086,12 +2086,12 @@ const qChanges = [ * @param {prng.PRNG} gen */ (y, gen) => { // insert embed - const ytext = y.getText('text') + const ytext = y.get('text') const insertPos = prng.int32(gen, 0, ytext.length) if (prng.bool(gen)) { - ytext.insertEmbed(insertPos, { image: 'https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png' }) + ytext.insert(insertPos, [{ image: 'https://user-images.githubusercontent.com/5553757/48975307-61efb100-f06d-11e8-9177-ee895e5916e5.png' }]) } else { - ytext.insertEmbed(insertPos, new Y.Map([[prng.word(gen), prng.word(gen)]])) + ytext.insert(insertPos, [new Y.Type([[prng.word(gen), prng.word(gen)]])]) } }, /** @@ -2099,7 +2099,7 @@ const qChanges = [ * @param {prng.PRNG} gen */ (y, gen) => { // delete text - const ytext = y.getText('text') + const ytext = y.get('text') const contentLen = ytext.toString().length const insertPos = prng.int32(gen, 0, contentLen) const overwrite = math.min(prng.int32(gen, 0, contentLen - insertPos), 2) @@ -2110,7 +2110,7 @@ const qChanges = [ * @param {prng.PRNG} gen */ (y, gen) => { // format text - const ytext = y.getText('text') + const ytext = y.get('text') const contentLen = ytext.toString().length const insertPos = prng.int32(gen, 0, contentLen) const overwrite = math.min(prng.int32(gen, 0, contentLen - insertPos), 2) @@ -2122,7 +2122,7 @@ const qChanges = [ * @param {prng.PRNG} gen */ (y, gen) => { // insert codeblock - const ytext = y.getText('text') + const ytext = y.get('text') const insertPos = prng.int32(gen, 0, ytext.toString().length) const text = charCounter++ + prng.word(gen) const d = delta.create() @@ -2134,7 +2134,7 @@ const qChanges = [ * @param {prng.PRNG} gen */ (y, gen) => { // complex delta op - const ytext = y.getText('text') + const ytext = y.get('text') const contentLen = ytext.toString().length let currentPos = math.max(0, prng.int32(gen, 0, contentLen - 1)) const d = delta.create().retain(currentPos) @@ -2191,7 +2191,7 @@ export const testAttributionManagerDefaultPerformance = tc => { const MaxDeletionLength = 5 // 25% chance of deletion const MaxInsertionLength = 5 const ydoc = new Y.Doc() - const ytext = ydoc.getText() + const ytext = ydoc.get() for (let i = 0; i < N; i++) { if (prng.bool(tc.prng) && prng.bool(tc.prng) && ytext.length > 0) { const index = prng.int31(tc.prng, 0, ytext.length - 1) diff --git a/tests/y-xml.tests.js b/tests/y-xml.tests.js index 41e982a3..c689b7dd 100644 --- a/tests/y-xml.tests.js +++ b/tests/y-xml.tests.js @@ -5,24 +5,24 @@ import * as delta from 'lib0/delta' export const testCustomTypings = () => { const ydoc = new Y.Doc() - const ymap = ydoc.getMap() + const ymap = ydoc.get() /** - * @type {Y.XmlElement<{ num: number, str: string, [k:string]: object|number|string }>} + * @type {Y.Type<{ attrs: { num: number, str: string, [k:string]: number|string } }>} */ - const yxml = ymap.set('yxml', new Y.XmlElement('test')) + const yxml = ymap.setAttr('yxml', new Y.Type('test')) /** * @type {number|undefined} */ - const num = yxml.getAttribute('num') + const num = yxml.getAttr('num') /** * @type {string|undefined} */ - const str = yxml.getAttribute('str') + const str = yxml.getAttr('str') /** * @type {object|number|string|undefined} */ - const dtrn = yxml.getAttribute('dtrn') - const attrs = yxml.getAttributes() + const dtrn = yxml.getAttr('dtrn') + const attrs = yxml.getAttrs() /** * @type {object|number|string|undefined} */ @@ -35,10 +35,10 @@ export const testCustomTypings = () => { */ export const testSetProperty = tc => { const { testConnector, users, xml0, xml1 } = init(tc, { users: 2 }) - xml0.setAttribute('height', '10') - t.assert(xml0.getAttribute('height') === '10', 'Simple set+get works') + xml0.setAttr('height', '10') + t.assert(xml0.getAttr('height') === '10', 'Simple set+get works') testConnector.flushAllMessages() - t.assert(xml1.getAttribute('height') === '10', 'Simple set+get works (remote)') + t.assert(xml1.getAttr('height') === '10', 'Simple set+get works (remote)') compare(users) } @@ -47,15 +47,14 @@ export const testSetProperty = tc => { */ export const testHasProperty = tc => { const { testConnector, users, xml0, xml1 } = init(tc, { users: 2 }) - xml0.setAttribute('height', '10') - t.assert(xml0.hasAttribute('height'), 'Simple set+has works') + xml0.setAttr('height', '10') + t.assert(xml0.hasAttr('height'), 'Simple set+has works') testConnector.flushAllMessages() - t.assert(xml1.hasAttribute('height'), 'Simple set+has works (remote)') - - xml0.removeAttribute('height') - t.assert(!xml0.hasAttribute('height'), 'Simple set+remove+has works') + t.assert(xml1.hasAttr('height'), 'Simple set+has works (remote)') + xml0.deleteAttr('height') + t.assert(!xml0.hasAttr('height'), 'Simple set+remove+has works') testConnector.flushAllMessages() - t.assert(!xml1.hasAttribute('height'), 'Simple set+remove+has works (remote)') + t.assert(!xml1.hasAttr('height'), 'Simple set+remove+has works (remote)') compare(users) } @@ -64,13 +63,13 @@ export const testHasProperty = tc => { */ export const testYtextAttributes = _tc => { const ydoc = new Y.Doc() - const ytext = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText)) + const ytext = ydoc.get('') ytext.observe(event => { t.assert(event.delta.attrs.test?.type === 'insert') }) - ytext.setAttribute('test', 42) - t.compare(ytext.getAttribute('test'), 42) - t.compare(ytext.getAttributes(), { test: 42 }) + ytext.setAttr('test', 42) + t.compare(ytext.getAttr('test'), 42) + t.compare(ytext.getAttrs(), { test: 42 }) } /** @@ -78,15 +77,12 @@ export const testYtextAttributes = _tc => { */ export const testSiblings = _tc => { const ydoc = new Y.Doc() - const yxml = ydoc.getXmlFragment() - const first = new Y.XmlText() - const second = new Y.XmlElement('p') + const yxml = ydoc.get() + const first = new Y.Type() + const second = new Y.Type('p') yxml.insert(0, [first, second]) - t.assert(first.nextSibling === second) - t.assert(second.prevSibling === first) t.assert(first.parent === /** @type {Y.AbstractType} */ (yxml)) t.assert(yxml.parent === null) - t.assert(yxml.firstChild === first) } /** @@ -94,13 +90,13 @@ export const testSiblings = _tc => { */ export const testInsertafter = _tc => { const ydoc = new Y.Doc() - const yxml = ydoc.getXmlFragment() - const first = new Y.XmlText() - const second = new Y.XmlElement('p') - const third = new Y.XmlElement('p') + const yxml = ydoc.get() + const first = new Y.Type() + const second = new Y.Type('p') + const third = new Y.Type('p') - const deepsecond1 = new Y.XmlElement('span') - const deepsecond2 = new Y.XmlText() + const deepsecond1 = new Y.Type('span') + const deepsecond2 = new Y.Type() second.insertAfter(null, [deepsecond1]) second.insertAfter(deepsecond1, [deepsecond2]) @@ -114,8 +110,8 @@ export const testInsertafter = _tc => { t.compareArrays(yxml.toArray(), [first, second, third]) t.fails(() => { - const el = new Y.XmlElement('p') - el.insertAfter(deepsecond1, [new Y.XmlText()]) + const el = new Y.Type('p') + el.insertAfter(deepsecond1, [new Y.Type()]) }) } @@ -124,14 +120,14 @@ export const testInsertafter = _tc => { */ export const testClone = _tc => { const ydoc = new Y.Doc() - const yxml = ydoc.getXmlFragment() - const first = new Y.XmlText('text') - const second = new Y.XmlElement('p') - const third = new Y.XmlElement('p') + const yxml = ydoc.get() + const first = new Y.Type('text') + const second = new Y.Type('p') + const third = new Y.Type('p') yxml.push([first, second, third]) t.compareArrays(yxml.toArray(), [first, second, third]) const cloneYxml = yxml.clone() - ydoc.getArray('copyarr').insert(0, [cloneYxml]) + ydoc.get('copyarr').insert(0, [cloneYxml]) t.assert(cloneYxml.length === 3) t.compare(cloneYxml.toJSON(), yxml.toJSON()) } @@ -141,7 +137,7 @@ export const testClone = _tc => { */ export const testFormattingBug = _tc => { const ydoc = new Y.Doc() - const yxml = /** @type {Y.XmlText} */ (ydoc.get('', Y.XmlText)) + const yxml = ydoc.get() const q = delta.create() .insert('A', { em: {}, strong: {} }) .insert('B', { em: {} }) @@ -155,9 +151,9 @@ export const testFormattingBug = _tc => { */ export const testElement = _tc => { const ydoc = new Y.Doc() - const yxmlel = ydoc.getXmlElement() - const text1 = new Y.XmlText('text1') - const text2 = new Y.XmlText('text2') + const yxmlel = ydoc.get() + const text1 = new Y.Type('text1') + const text2 = new Y.Type('text2') yxmlel.insert(0, [text1, text2]) t.compareArrays(yxmlel.toArray(), [text1, text2]) } @@ -167,12 +163,12 @@ export const testElement = _tc => { */ export const testFragmentAttributedContent = _tc => { const ydoc = new Y.Doc({ gc: false }) - const yfragment = new Y.XmlFragment() - const elem1 = new Y.XmlText('hello') - const elem2 = new Y.XmlElement() - const elem3 = new Y.XmlText('world') + const yfragment = new Y.Type() + const elem1 = new Y.Type('hello') + const elem2 = new Y.Type() + const elem3 = new Y.Type('world') yfragment.insert(0, [elem1, elem2]) - ydoc.getArray().insert(0, [yfragment]) + ydoc.get().insert(0, [yfragment]) let attributionManager = Y.noAttributionsManager ydoc.on('afterTransaction', tr => { // attributionManager = new TwosetAttributionManager(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertedAt', 42), new Y.Attribution('insert', 'kevin')]), createIdMapFromIdSet(tr.deleteSet, [new Y.Attribution('delete', 'kevin')])) @@ -196,10 +192,10 @@ export const testFragmentAttributedContent = _tc => { */ export const testElementAttributedContent = _tc => { const ydoc = new Y.Doc({ gc: false }) - const yelement = ydoc.getXmlElement('p') - const elem1 = new Y.XmlText('hello') - const elem2 = new Y.XmlElement('span') - const elem3 = new Y.XmlText('world') + const yelement = ydoc.get('p') + const elem1 = new Y.Type('hello') + const elem2 = new Y.Type('span') + const elem3 = new Y.Type('world') yelement.insert(0, [elem1, elem2]) let attributionManager = Y.noAttributionsManager ydoc.on('afterTransaction', tr => { @@ -210,9 +206,9 @@ export const testElementAttributedContent = _tc => { ydoc.transact(() => { yelement.delete(0, 1) yelement.insert(1, [elem3]) - yelement.setAttribute('key', '42') + yelement.setAttr('key', '42') }) - const expectedContent = delta.create('UNDEFINED').insert([elem1], null, { delete: [] }).insert([elem2]).insert([elem3], null, { insert: [] }).set('key', '42', { insert: [] }) + const expectedContent = delta.create('UNDEFINED').insert([elem1], null, { delete: [] }).insert([elem2]).insert([elem3], null, { insert: [] }).setAttr('key', '42', { insert: [] }) const attributedContent = yelement.getContent(attributionManager) console.log('children', attributedContent.toJSON()) console.log('attributes', attributedContent) @@ -221,15 +217,15 @@ export const testElementAttributedContent = _tc => { t.group('test getContentDeep', () => { const expectedContent = delta.create('UNDEFINED') .insert( - [delta.text().insert('hello', null, { delete: [] })], + [delta.create().insert('hello', null, { delete: [] })], null, { delete: [] } ) .insert([delta.create('span')]) .insert([ - delta.text().insert('world', null, { insert: [] }) + delta.create().insert('world', null, { insert: [] }) ], null, { insert: [] }) - .set('key', '42', { insert: [] }) + .setAttr('key', '42', { insert: [] }) .done() const attributedContent = yelement.getContentDeep(attributionManager) console.log('children', JSON.stringify(attributedContent.toJSON().children, null, 2)) @@ -247,19 +243,19 @@ export const testElementAttributedContent = _tc => { */ export const testElementAttributedContentViaDiffer = _tc => { const ydocV1 = new Y.Doc() - ydocV1.getXmlElement('p').insert(0, [new Y.XmlText('hello'), new Y.XmlElement('span')]) + ydocV1.get('p').insert(0, [new Y.Type('hello'), new Y.Type('span')]) const ydoc = new Y.Doc() Y.applyUpdate(ydoc, Y.encodeStateAsUpdate(ydocV1)) - const yelement = ydoc.getXmlElement('p') + const yelement = ydoc.get('p') const elem2 = yelement.get(1) // new Y.XmlElement('span') - const elem3 = new Y.XmlText('world') + const elem3 = new Y.Type('world') ydoc.transact(() => { yelement.delete(0, 1) yelement.insert(1, [elem3]) - yelement.setAttribute('key', '42') + yelement.setAttr('key', '42') }) const attributionManager = Y.createAttributionManagerFromDiff(ydocV1, ydoc) - const expectedContent = delta.create('UNDEFINED').insert([delta.create().insert('hello')], null, { delete: [] }).insert([elem2.getContentDeep()]).insert([delta.create().insert('world', null, { insert: [] })], null, { insert: [] }).set('key', '42', { insert: [] }) + const expectedContent = delta.create('UNDEFINED').insert([delta.create().insert('hello')], null, { delete: [] }).insert([elem2.getContentDeep()]).insert([delta.create().insert('world', null, { insert: [] })], null, { insert: [] }).setAttr('key', '42', { insert: [] }) const attributedContent = yelement.getContentDeep(attributionManager) console.log('children', attributedContent.toJSON().children) console.log('attributes', attributedContent.toJSON().attrs) @@ -277,7 +273,7 @@ export const testElementAttributedContentViaDiffer = _tc => { .insert([ delta.create().insert('world', null, { insert: [] }) ], null, { insert: [] }) - .set('key', '42', { insert: [] }) + .setAttr('key', '42', { insert: [] }) const attributedContent = yelement.getContentDeep(attributionManager) console.log('children', JSON.stringify(attributedContent.toJSON().children, null, 2)) console.log('cs expec', JSON.stringify(expectedContent.toJSON(), null, 2)) @@ -301,7 +297,7 @@ export const testElementAttributedContentViaDiffer = _tc => { .insert([ delta.create().insert('bigworld', null, { insert: [] }) ], null, { insert: [] }) - .set('key', '42', { insert: [] }) + .setAttr('key', '42', { insert: [] }) const attributedContent = yelement.getContentDeep(attributionManager) console.log('children', JSON.stringify(attributedContent.toJSON().children, null, 2)) console.log('cs expec', JSON.stringify(expectedContent.toJSON(), null, 2)) @@ -315,7 +311,7 @@ export const testElementAttributedContentViaDiffer = _tc => { t.info('expecting diffingAttributionManager to auto update itself') const expectedContent = delta.create('UNDEFINED').insert([delta.create('span')]).insert([ delta.create().insert('bigworld') - ]).set('key', '42') + ]).setAttr('key', '42') const attributedContent = yelement.getContentDeep(attributionManager) console.log('children', JSON.stringify(attributedContent.toJSON().children, null, 2)) console.log('cs expec', JSON.stringify(expectedContent.toJSON(), null, 2)) @@ -333,21 +329,21 @@ export const testAttributionManagerSimpleExample = _tc => { const ydoc = new Y.Doc() ydoc.clientID = 0 // create some initial content - ydoc.getXmlFragment().insert(0, [new Y.XmlText('hello world')]) + ydoc.get().insert(0, [new Y.Type('hello world')]) const ydocFork = new Y.Doc() ydocFork.clientID = 1 Y.applyUpdate(ydocFork, Y.encodeStateAsUpdate(ydoc)) // modify the fork // append a span element - ydocFork.getXmlFragment().insert(1, [new Y.XmlElement('span')]) - const ytext = /** @type {Y.XmlText} */ (ydocFork.getXmlFragment().get(0)) + ydocFork.get().insert(1, [new Y.Type('span')]) + const ytext = ydocFork.get().get(0) // make "hello" italic ytext.format(0, 5, { italic: true }) ytext.insert(11, 'deleteme') ytext.delete(11, 8) ytext.insert(11, '!') // highlight the changes - console.log(JSON.stringify(ydocFork.getXmlFragment().getContentDeep(Y.createAttributionManagerFromDiff(ydoc, ydocFork)), null, 2)) + console.log(JSON.stringify(ydocFork.get().getContentDeep(Y.createAttributionManagerFromDiff(ydoc, ydocFork)), null, 2)) /* => { "children": {