implement and test getContent on all types

This commit is contained in:
Kevin Jahns
2025-04-28 17:06:32 +02:00
parent b3171c535f
commit 4f840247a3
11 changed files with 337 additions and 83 deletions

View File

@@ -109,7 +109,10 @@ export {
diffIdMap,
diffIdSet,
AttributionItem as Attribution,
encodeIdMap
encodeIdMap,
createIdMapFromIdSet,
TwosetAttributionManager,
noAttributionsManager
} from './internals.js'
const glo = /** @type {any} */ (typeof globalThis !== 'undefined'

View File

@@ -10,9 +10,12 @@ import {
ContentAny,
ContentBinary,
getItemCleanStart,
ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item, // eslint-disable-line
ContentDoc, YText, YArray, UpdateEncoderV1, UpdateEncoderV2, Doc, Snapshot, Transaction, EventHandler, YEvent, Item,
createAttributionFromAttrs, // eslint-disable-line
} from '../internals.js'
import * as delta from '../utils/Delta.js'
import * as array from 'lib0/array'
import * as map from 'lib0/map'
import * as iterator from 'lib0/iterator'
import * as error from 'lib0/error'
@@ -466,6 +469,42 @@ export const typeListToArray = type => {
return cs
}
/**
* 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 MapType
* @param {AbstractType<any>} type
* @param {import('../internals.js').AbstractAttributionManager} am
* @return {delta.Delta<Array<MapType>>} The Delta representation of this type.
*
* @private
* @function
*/
export const typeListGetContent = (type, am) => {
type.doc ?? warnPrematureAccess()
const d = /** @type {delta.DeltaBuilder<Array<MapType>>} */ (delta.create())
/**
* @type {Array<import('../internals.js').AttributedContent<any>>}
*/
const cs = []
for (let item = type._start; item !== null; cs.length = 0) {
// populate cs
for (; item !== null && cs.length < 50; item = item.right) {
am.readContent(cs, item)
}
for (let i = 0; i < cs.length; i++) {
const { content, deleted, attrs } = cs[i]
const attribution = createAttributionFromAttrs(attrs, deleted)
d.insert(content.getContent(), null, attribution)
}
}
return d.done()
}
/**
* @param {AbstractType<any>} type
* @param {Snapshot} snapshot
@@ -913,6 +952,71 @@ export const typeMapGetAll = (parent) => {
return res
}
/**
* @template MapType
* @typedef {{ [key: string]: { prevValue: MapType | undefined, value: MapType | undefined, attribution: any } }} MapAttributedContent
*/
/**
* 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 MapType
* @param {AbstractType<any>} parent
* @param {import('../internals.js').AbstractAttributionManager} am
* @return {MapAttributedContent<MapType>} The Delta representation of this type.
*
* @private
* @function
*/
export const typeMapGetContent = (parent, am) => {
/**
* @type {MapAttributedContent<MapType>}
*/
const mapcontent = {}
parent.doc ?? warnPrematureAccess()
parent._map.forEach((item, key) => {
/**
* @type {Array<import('../internals.js').AttributedContent<any>>}
*/
const cs = []
am.readContent(cs, item)
const { deleted, attrs, content } = cs[cs.length - 1]
const c = array.last(content.getContent())
const attribution = createAttributionFromAttrs(attrs, deleted)
if (deleted) {
mapcontent[key] = { prevValue: c, value: undefined, attribution }
} else {
/**
* @type {Array<import('../internals.js').AttributedContent<any>>}
*/
let cs = []
for (let prevItem = item.left; prevItem != null; prevItem = prevItem.left) {
/**
* @type {Array<import('../internals.js').AttributedContent<any>>}
*/
const tmpcs = []
am.readContent(tmpcs, prevItem)
cs = tmpcs.concat(cs)
if (cs[0].attrs == null) {
cs.splice(0, cs.findIndex(c => c.attrs != null))
break
}
if (cs.length > 0) {
cs.length = 1
}
}
const prevValue = cs.length > 0 ? array.last(cs[0].content.getContent()) : undefined
mapcontent[key] = { prevValue, value: c, attribution }
}
})
return mapcontent
}
/**
* @param {AbstractType<any>} parent
* @param {string} key

View File

@@ -17,9 +17,10 @@ import {
callTypeObservers,
transact,
warnPrematureAccess,
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item // eslint-disable-line
ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, // eslint-disable-line
AbstractAttributionManager
} from '../internals.js'
import { typeListSlice } from './AbstractType.js'
import { typeListGetContent, typeListSlice } from './AbstractType.js'
/**
* Event that describes the changes on a YArray
@@ -207,6 +208,22 @@ export class YArray extends AbstractType {
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 {import('../utils/Delta.js').Delta<Array<T>>} The Delta representation of this type.
*
* @public
*/
getContent (am) {
return typeListGetContent(this, am)
}
/**
* Returns a portion of this YArray into a JavaScript Array selected
* from start to end (end not included).

View File

@@ -15,17 +15,13 @@ import {
transact,
warnPrematureAccess,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Transaction, Item, // eslint-disable-line
createAttributionFromAttrs
createAttributionFromAttrs,
typeMapGetContent
} from '../internals.js'
import * as array from 'lib0/array'
import * as iterator from 'lib0/iterator'
/**
* @template MapType
* @typedef {{ [key: string]: { prevValue: MapType | undefined, value: MapType | undefined, attribution: any } }} MapAttributedContent
*/
/**
* @template T
* @extends YEvent<YMap<T>>
@@ -201,51 +197,12 @@ export class YMap extends AbstractType {
* attribution `{ isDeleted: true, .. }`.
*
* @param {import('../internals.js').AbstractAttributionManager} am
* @return {MapAttributedContent<MapType>} The Delta representation of this type.
* @return {import('./AbstractType.js').MapAttributedContent<MapType>} The Delta representation of this type.
*
* @public
*/
getContent (am) {
/**
* @type {MapAttributedContent<MapType>}
*/
const mapcontent = {}
this._map.forEach((item, key) => {
/**
* @type {Array<import('../internals.js').AttributedContent<any>>}
*/
const cs = []
am.readContent(cs, item)
const { deleted, attrs, content } = cs[cs.length - 1]
const c = array.last(content.getContent())
const attribution = createAttributionFromAttrs(attrs, deleted)
if (deleted) {
mapcontent[key] = { prevValue: c, value: undefined, attribution }
} else {
/**
* @type {Array<import('../internals.js').AttributedContent<any>>}
*/
let cs = []
for (let prevItem = item.left; prevItem != null; prevItem = prevItem.left) {
/**
* @type {Array<import('../internals.js').AttributedContent<any>>}
*/
const tmpcs = []
am.readContent(tmpcs, prevItem)
cs = tmpcs.concat(cs)
if (cs[0].attrs == null) {
cs.splice(0, cs.findIndex(c => c.attrs != null))
break
}
if (cs.length > 0) {
cs.length = 1
}
}
const prevValue = cs.length > 0 ? array.last(cs[0].content.getContent()) : undefined
mapcontent[key] = { prevValue, value: c, attribution }
}
})
return mapcontent
return typeMapGetContent(this, am)
}
/**

View File

@@ -1000,13 +1000,13 @@ export class YText extends AbstractType {
* attribution `{ isDeleted: true, .. }`.
*
* @param {AbstractAttributionManager} am
* @return {import('../utils/Delta.js').Delta} The Delta representation of this type.
* @return {import('../utils/Delta.js').Delta<import('../utils/Delta.js').TextDeltaContent>} The Delta representation of this type.
*
* @public
*/
getContent (am = noAttributionsManager) {
this.doc ?? warnPrematureAccess()
const d = delta.create()
const d = delta.createTextDelta()
/**
* @type {Array<import('../internals.js').AttributedContent<any>>}
*/

View File

@@ -11,7 +11,9 @@ import {
typeMapGetAllSnapshot,
typeListForEach,
YXmlElementRefID,
Snapshot, YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item // eslint-disable-line
typeMapGetContent,
noAttributionsManager,
Snapshot, YXmlText, ContentType, AbstractType, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item, // eslint-disable-line
} from '../internals.js'
/**
@@ -206,6 +208,23 @@ export class YXmlElement extends YXmlFragment {
return /** @type {any} */ (snapshot ? typeMapGetAllSnapshot(this, snapshot) : typeMapGetAll(this))
}
/**
* Render the difference to another ydoc (which can be empty) and highlight the differences with
* attributions.
*
* Note that deleted content that was not deleted in prevYdoc is rendered as an insertion with the
* attribution `{ isDeleted: true, .. }`.
*
* @param {import('../internals.js').AbstractAttributionManager} am
*
* @public
*/
getContent (am = noAttributionsManager) {
const attributes = typeMapGetContent(this, am)
const { children } = super.getContent(am)
return { children, attributes }
}
/**
* Creates a Dom Element that mirrors this YXmlElement.
*

View File

@@ -18,7 +18,9 @@ import {
typeListGet,
typeListSlice,
warnPrematureAccess,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook // eslint-disable-line
noAttributionsManager,
UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, ContentType, Transaction, Item, YXmlText, YXmlHook, // eslint-disable-line
typeListGetContent
} from '../internals.js'
import * as error from 'lib0/error'
@@ -377,6 +379,17 @@ export class YXmlFragment extends AbstractType {
return typeListToArray(this)
}
/**
* Calculate the attributed content using the attribution manager.
*
* @param {import('../internals.js').AbstractAttributionManager} am
* @return {{ children: import('../utils/Delta.js').Delta<Array<YXmlElement|YXmlText|YXmlHook>> }}
*/
getContent (am = noAttributionsManager) {
const children = typeListGetContent(this, am)
return { children }
}
/**
* Appends content to this YArray.
*

View File

@@ -2,7 +2,8 @@ import * as object from 'lib0/object'
import * as fun from 'lib0/function'
/**
* @typedef {InsertOp|RetainOp|DeleteOp} DeltaOp
* @template {string|Array<any>|{[key: string]: any}} Content
* @typedef {InsertOp<Content>|RetainOp|DeleteOp} DeltaOp
*/
/**
@@ -13,9 +14,12 @@ import * as fun from 'lib0/function'
* @typedef {{ [key: string]: any }} FormattingAttributes
*/
/**
* @template {string|Array<any>|{[key: string]: any}} Content
*/
export class InsertOp {
/**
* @param {string} insert
* @param {Content} insert
* @param {FormattingAttributes|null} attributes
* @param {Attribution|null} attribution
*/
@@ -60,16 +64,19 @@ export class RetainOp {
}
}
/**
* @template {string|Array<any>|{[key: string]: any}} Content
*/
export class Delta {
constructor () {
/**
* @type {Array<DeltaOp>}
* @type {Array<DeltaOp<Content>>}
*/
this.ops = []
}
/**
* @param {Delta} d
* @param {Delta<Content>} d
* @return {boolean}
*/
equals (d) {
@@ -85,9 +92,9 @@ export class Delta {
}
case InsertOp: {
if (
!fun.equalityDeep(/** @type {InsertOp} */ (op).insert, /** @type {InsertOp} */ (dop).insert)
|| !fun.equalityDeep(/** @type {InsertOp} */ (op).attributes, /** @type {InsertOp} */ (dop).attributes)
|| !fun.equalityDeep(/** @type {InsertOp} */ (op).attribution, /** @type {InsertOp} */ (dop).attribution)
!fun.equalityDeep(/** @type {InsertOp<Content>} */ (op).insert, /** @type {InsertOp<Content>} */ (dop).insert)
|| !fun.equalityDeep(/** @type {InsertOp<Content>} */ (op).attributes, /** @type {InsertOp<Content>} */ (dop).attributes)
|| !fun.equalityDeep(/** @type {InsertOp<Content>} */ (op).attribution, /** @type {InsertOp<Content>} */ (dop).attribution)
) {
return false
}
@@ -126,6 +133,10 @@ const mergeAttrs = (a, b) => {
return merged
}
/**
* @template {string|Array<any>|{[key: string]: any}} Content
* @extends Delta<Content>
*/
export class DeltaBuilder extends Delta {
constructor () {
super()
@@ -139,7 +150,7 @@ export class DeltaBuilder extends Delta {
this.usedAttribution = null
/**
* @private
* @type {DeltaOp?}
* @type {DeltaOp<Content>?}
*/
this._lastOp = null
}
@@ -164,7 +175,7 @@ export class DeltaBuilder extends Delta {
}
/**
* @param {string} insert
* @param {Content} insert
* @param {FormattingAttributes?} attributes
* @param {Attribution?} attribution
* @return {this}
@@ -173,7 +184,15 @@ export class DeltaBuilder extends Delta {
const mergedAttributes = attributes == null ? this.usedAttributes : mergeAttrs(this.usedAttributes, attributes)
const mergedAttribution = attribution == null ? this.usedAttribution : mergeAttrs(this.usedAttribution, attribution)
if (this._lastOp instanceof InsertOp && (mergedAttributes === this._lastOp.attributes || fun.equalityDeep(mergedAttributes, this._lastOp.attributes)) && (mergedAttribution === this._lastOp.attribution || fun.equalityDeep(mergedAttribution, this._lastOp.attribution))) {
this._lastOp.insert += insert
if (insert.constructor === String) {
// @ts-ignore
this._lastOp.insert += insert
} else if (insert.constructor === Array && this._lastOp.insert.constructor === Array) {
// @ts-ignore
this._lastOp.insert.push(...insert)
} else {
this.ops.push(this._lastOp = new InsertOp(insert, mergedAttributes, mergedAttribution))
}
} else {
this.ops.push(this._lastOp = new InsertOp(insert, mergedAttributes, mergedAttribution))
}
@@ -211,7 +230,7 @@ export class DeltaBuilder extends Delta {
}
/**
* @return {Delta}
* @return {Delta<Content>}
*/
done () {
return this
@@ -219,3 +238,23 @@ export class DeltaBuilder extends Delta {
}
export const create = () => new DeltaBuilder()
/**
* @typedef {string | { [key: string]: any }} TextDeltaContent
*/
/**
* @template {TextDeltaContent} Content
* @return {DeltaBuilder<Content>}
*/
export const createTextDelta = () => new DeltaBuilder()
/**
* @typedef {Array<any>} ArrayDeltaContent
*/
/**
* @template {ArrayDeltaContent} Content
* @return {DeltaBuilder<Content>}
*/
export const createArrayDelta = () => new DeltaBuilder()

View File

@@ -5,6 +5,21 @@ import * as delta from '../src/utils/Delta.js'
* @param {t.TestCase} _tc
*/
export const testDelta = _tc => {
const d = delta.create().insert('hello').insert(' ').useAttributes({ bold: true }).insert('world').useAttribution({ creator: 'tester' }).insert('!').done()
t.compare(d.toJSON().ops, [{ insert: 'hello ' }, { insert: 'world', attributes: { bold: true } }, { insert: '!', attributes: { bold: true }, attribution: { creator: 'tester' } }])
const d = delta.create().insert('hello').insert(' ').useAttributes({ bold: true }).insert('world').useAttribution({ insert: ['tester'] }).insert('!').done()
t.compare(d.toJSON().ops, [{ insert: 'hello ' }, { insert: 'world', attributes: { bold: true } }, { insert: '!', attributes: { bold: true }, attribution: { insert: ['tester'] } }])
}
/**
* @param {t.TestCase} _tc
*/
export const testDeltaMerging = _tc => {
const d = delta.create()
.insert('hello')
.insert('world')
.insert(' ', { italic: true })
.insert({})
.insert([1])
.insert([2])
.done()
t.compare(d.toJSON().ops, [{ insert: 'helloworld' }, { insert: ' ', attributes: { italic: true } }, { insert: {} }, { insert: [1, 2] }])
}

View File

@@ -4,13 +4,14 @@ import * as t from 'lib0/testing'
import * as prng from 'lib0/prng'
import * as math from 'lib0/math'
import * as env from 'lib0/environment'
import * as delta from '../src/utils/Delta.js'
const isDevMode = env.getVariable('node_env') === 'development'
/**
* @param {t.TestCase} tc
* @param {t.TestCase} _tc
*/
export const testBasicUpdate = tc => {
export const testBasicUpdate = _tc => {
const doc1 = new Y.Doc()
const doc2 = new Y.Doc()
doc1.getArray('array').insert(0, ['hi'])
@@ -20,9 +21,9 @@ export const testBasicUpdate = tc => {
}
/**
* @param {t.TestCase} tc
* @param {t.TestCase} _tc
*/
export const testFailsObjectManipulationInDevMode = tc => {
export const testFailsObjectManipulationInDevMode = _tc => {
if (isDevMode) {
t.info('running in dev mode')
const doc = new Y.Doc()
@@ -42,9 +43,9 @@ export const testFailsObjectManipulationInDevMode = tc => {
}
/**
* @param {t.TestCase} tc
* @param {t.TestCase} _tc
*/
export const testSlice = tc => {
export const testSlice = _tc => {
const doc1 = new Y.Doc()
const arr = doc1.getArray('array')
arr.insert(0, [1, 2, 3])
@@ -57,9 +58,9 @@ export const testSlice = tc => {
}
/**
* @param {t.TestCase} tc
* @param {t.TestCase} _tc
*/
export const testArrayFrom = tc => {
export const testArrayFrom = _tc => {
const doc1 = new Y.Doc()
const db1 = doc1.getMap('root')
const nestedArray1 = Y.Array.from([0, 1, 2])
@@ -70,9 +71,9 @@ export const testArrayFrom = tc => {
/**
* Debugging yjs#297 - a critical bug connected to the search-marker approach
*
* @param {t.TestCase} tc
* @param {t.TestCase} _tc
*/
export const testLengthIssue = tc => {
export const testLengthIssue = _tc => {
const doc1 = new Y.Doc()
const arr = doc1.getArray('array')
arr.push([0, 1, 2, 3])
@@ -99,9 +100,9 @@ export const testLengthIssue = tc => {
/**
* Debugging yjs#314
*
* @param {t.TestCase} tc
* @param {t.TestCase} _tc
*/
export const testLengthIssue2 = tc => {
export const testLengthIssue2 = _tc => {
const doc = new Y.Doc()
const next = doc.getArray()
doc.transact(() => {
@@ -288,7 +289,7 @@ export const testNestedObserverEvents = tc => {
* @type {Array<number>}
*/
const vals = []
array0.observe(e => {
array0.observe(() => {
if (array0.length === 1) {
// inserting, will call this observer again
// we expect that this observer is called after this event handler finishedn
@@ -491,9 +492,9 @@ export const testEventTargetIsSetCorrectlyOnRemote = tc => {
}
/**
* @param {t.TestCase} tc
* @param {t.TestCase} _tc
*/
export const testIteratingArrayContainingTypes = tc => {
export const testIteratingArrayContainingTypes = _tc => {
const y = new Y.Doc()
const arr = y.getArray('arr')
const numItems = 10
@@ -509,6 +510,31 @@ export const testIteratingArrayContainingTypes = tc => {
y.destroy()
}
/**
* @param {t.TestCase} _tc
*/
export const testAttributedContent = _tc => {
const ydoc = new Y.Doc({ gc: false })
const yarray = ydoc.getArray()
yarray.insert(0, [1, 2])
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')]))
attributionManager = new Y.TwosetAttributionManager(Y.createIdMapFromIdSet(tr.insertSet, []), Y.createIdMapFromIdSet(tr.deleteSet, []))
})
t.group('insert / delete', () => {
ydoc.transact(() => {
yarray.delete(0, 1)
yarray.insert(1, [42])
})
let expectedContent = delta.createArrayDelta().insert([1], null, { delete: [] }).insert([2]).insert([42], null, { insert: [] })
let attributedContent = yarray.getContent(attributionManager)
console.log(attributedContent.toJSON().ops)
t.assert(attributedContent.equals(expectedContent))
})
}
let _uniqueNumber = 0
const getUniqueNumber = () => _uniqueNumber++

View File

@@ -1,6 +1,7 @@
import * as Y from '../src/index.js'
import { init, compare } from './testHelper.js'
import * as t from 'lib0/testing'
import * as delta from '../src/utils/Delta.js'
export const testCustomTypings = () => {
const ydoc = new Y.Doc()
@@ -220,3 +221,63 @@ export const testElement = _tc => {
yxmlel.insert(0, [text1, text2])
t.compareArrays(yxmlel.toArray(), [text1, text2])
}
/**
* @param {t.TestCase} _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')
yfragment.insert(0, [elem1, elem2])
ydoc.getArray().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')]))
attributionManager = new Y.TwosetAttributionManager(Y.createIdMapFromIdSet(tr.insertSet, []), Y.createIdMapFromIdSet(tr.deleteSet, []))
})
t.group('insert / delete', () => {
ydoc.transact(() => {
yfragment.delete(0, 1)
yfragment.insert(1, [elem3])
})
let expectedContent = delta.createArrayDelta().insert([elem1], null, { delete: [] }).insert([elem2]).insert([elem3], null, { insert: [] })
let attributedContent = yfragment.getContent(attributionManager)
console.log(attributedContent.children.toJSON().ops)
t.assert(attributedContent.children.equals(expectedContent))
t.compare(elem1.getContent(attributionManager).toJSON(), delta.createTextDelta().insert('hello', null, { delete: [] }).done().toJSON())
})
}
/**
* @param {t.TestCase} _tc
*/
export const testElementAttributedContent = _tc => {
const ydoc = new Y.Doc({ gc: false })
const yelement = ydoc.getXmlElement('p')
const elem1 = new Y.XmlElement('span')
const elem2 = new Y.XmlText('hello')
const elem3 = new Y.XmlText('world')
yelement.insert(0, [elem1, elem2])
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')]))
attributionManager = new Y.TwosetAttributionManager(Y.createIdMapFromIdSet(tr.insertSet, []), Y.createIdMapFromIdSet(tr.deleteSet, []))
})
t.group('insert / delete', () => {
ydoc.transact(() => {
yelement.delete(0, 1)
yelement.insert(1, [elem3])
yelement.setAttribute('key', '42')
})
let expectedContent = delta.createArrayDelta().insert([elem1], null, { delete: [] }).insert([elem2]).insert([elem3], null, { insert: [] })
let attributedContent = yelement.getContent(attributionManager)
console.log('children', attributedContent.children.toJSON().ops)
console.log('attributes', attributedContent.attributes)
t.assert(attributedContent.children.equals(expectedContent))
t.compare(attributedContent.attributes, { key: { prevValue: undefined, value: '42', attribution: { insert: [] } } })
})
}