diff --git a/src/index.js b/src/index.js index 67d63c3d..b7df9103 100644 --- a/src/index.js +++ b/src/index.js @@ -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' diff --git a/src/types/AbstractType.js b/src/types/AbstractType.js index a83f3a58..145c3b50 100644 --- a/src/types/AbstractType.js +++ b/src/types/AbstractType.js @@ -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} type + * @param {import('../internals.js').AbstractAttributionManager} am + * @return {delta.Delta>} The Delta representation of this type. + * + * @private + * @function + */ +export const typeListGetContent = (type, am) => { + type.doc ?? warnPrematureAccess() + const d = /** @type {delta.DeltaBuilder>} */ (delta.create()) + /** + * @type {Array>} + */ + 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} 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} parent + * @param {import('../internals.js').AbstractAttributionManager} am + * @return {MapAttributedContent} The Delta representation of this type. + * + * @private + * @function + */ +export const typeMapGetContent = (parent, am) => { + /** + * @type {MapAttributedContent} + */ + const mapcontent = {} + parent.doc ?? warnPrematureAccess() + parent._map.forEach((item, key) => { + /** + * @type {Array>} + */ + 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>} + */ + let cs = [] + for (let prevItem = item.left; prevItem != null; prevItem = prevItem.left) { + /** + * @type {Array>} + */ + 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} parent * @param {string} key diff --git a/src/types/YArray.js b/src/types/YArray.js index 8fd5c215..0fd94e37 100644 --- a/src/types/YArray.js +++ b/src/types/YArray.js @@ -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>} 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). diff --git a/src/types/YMap.js b/src/types/YMap.js index 369d9408..78f90e63 100644 --- a/src/types/YMap.js +++ b/src/types/YMap.js @@ -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> @@ -201,51 +197,12 @@ export class YMap extends AbstractType { * attribution `{ isDeleted: true, .. }`. * * @param {import('../internals.js').AbstractAttributionManager} am - * @return {MapAttributedContent} The Delta representation of this type. + * @return {import('./AbstractType.js').MapAttributedContent} The Delta representation of this type. * * @public */ getContent (am) { - /** - * @type {MapAttributedContent} - */ - const mapcontent = {} - this._map.forEach((item, key) => { - /** - * @type {Array>} - */ - 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>} - */ - let cs = [] - for (let prevItem = item.left; prevItem != null; prevItem = prevItem.left) { - /** - * @type {Array>} - */ - 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) } /** diff --git a/src/types/YText.js b/src/types/YText.js index 1f86b500..e00d6cb4 100644 --- a/src/types/YText.js +++ b/src/types/YText.js @@ -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} The Delta representation of this type. * * @public */ getContent (am = noAttributionsManager) { this.doc ?? warnPrematureAccess() - const d = delta.create() + const d = delta.createTextDelta() /** * @type {Array>} */ diff --git a/src/types/YXmlElement.js b/src/types/YXmlElement.js index 48029f69..b3a228aa 100644 --- a/src/types/YXmlElement.js +++ b/src/types/YXmlElement.js @@ -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. * diff --git a/src/types/YXmlFragment.js b/src/types/YXmlFragment.js index 544a18ce..72adf50c 100644 --- a/src/types/YXmlFragment.js +++ b/src/types/YXmlFragment.js @@ -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> }} + */ + getContent (am = noAttributionsManager) { + const children = typeListGetContent(this, am) + return { children } + } + /** * Appends content to this YArray. * diff --git a/src/utils/Delta.js b/src/utils/Delta.js index 668fe3b6..794b1251 100644 --- a/src/utils/Delta.js +++ b/src/utils/Delta.js @@ -2,7 +2,8 @@ import * as object from 'lib0/object' import * as fun from 'lib0/function' /** - * @typedef {InsertOp|RetainOp|DeleteOp} DeltaOp + * @template {string|Array|{[key: string]: any}} Content + * @typedef {InsertOp|RetainOp|DeleteOp} DeltaOp */ /** @@ -13,9 +14,12 @@ import * as fun from 'lib0/function' * @typedef {{ [key: string]: any }} FormattingAttributes */ +/** + * @template {string|Array|{[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|{[key: string]: any}} Content + */ export class Delta { constructor () { /** - * @type {Array} + * @type {Array>} */ this.ops = [] } /** - * @param {Delta} d + * @param {Delta} 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} */ (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) ) { return false } @@ -126,6 +133,10 @@ const mergeAttrs = (a, b) => { return merged } +/** + * @template {string|Array|{[key: string]: any}} Content + * @extends Delta + */ export class DeltaBuilder extends Delta { constructor () { super() @@ -139,7 +150,7 @@ export class DeltaBuilder extends Delta { this.usedAttribution = null /** * @private - * @type {DeltaOp?} + * @type {DeltaOp?} */ 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} */ 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} + */ +export const createTextDelta = () => new DeltaBuilder() + +/** + * @typedef {Array} ArrayDeltaContent + */ + +/** + * @template {ArrayDeltaContent} Content + * @return {DeltaBuilder} + */ +export const createArrayDelta = () => new DeltaBuilder() diff --git a/tests/delta.tests.js b/tests/delta.tests.js index eac5b6f1..0c43c327 100644 --- a/tests/delta.tests.js +++ b/tests/delta.tests.js @@ -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] }]) } diff --git a/tests/y-array.tests.js b/tests/y-array.tests.js index cae7650c..f508f03c 100644 --- a/tests/y-array.tests.js +++ b/tests/y-array.tests.js @@ -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} */ 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++ diff --git a/tests/y-xml.tests.js b/tests/y-xml.tests.js index e2743dae..faaf65c4 100644 --- a/tests/y-xml.tests.js +++ b/tests/y-xml.tests.js @@ -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: [] } } }) + }) +} +