From e53a6a022729835b1b2ecc8558675646e9cd7e96 Mon Sep 17 00:00:00 2001 From: Kevin Jahns Date: Tue, 16 Jun 2026 21:29:45 +0200 Subject: [PATCH] rename concept AttributionManager => Renderer definitely a breaking change to all users of the current attribution api --- attributing-content.md | 17 +++-- global.d.ts | 6 +- attribution-manager.md => renderer.md | 72 +++++++++--------- src/index.js | 2 +- src/utils/RelativePosition.js | 16 ++-- .../{AttributionManager.js => Renderer.js} | 48 ++++++------ src/utils/YEvent.js | 16 ++-- src/utils/delta-helpers.js | 8 +- ...manager-helpers.js => renderer-helpers.js} | 16 ++-- src/ytype.js | 74 ++++++++++--------- tests/attribution.tests.js | 44 +++++------ tests/delta.tests.js | 8 +- tests/y-array.tests.js | 8 +- tests/y-map.tests.js | 16 ++-- tests/y-text.tests.js | 32 ++++---- tests/y-xml.tests.js | 50 ++++++------- 16 files changed, 218 insertions(+), 215 deletions(-) rename attribution-manager.md => renderer.md (66%) rename src/utils/{AttributionManager.js => Renderer.js} (91%) rename src/utils/{attribution-manager-helpers.js => renderer-helpers.js} (84%) diff --git a/attributing-content.md b/attributing-content.md index a3c98511..5f45f8d3 100644 --- a/attributing-content.md +++ b/attributing-content.md @@ -51,11 +51,11 @@ const deleteSetDiff = Y.diffIdSet(deleteSet, Y.createDeleteSetFromStructStore(yd // assign attributes to the diff const attributedInsertions = createIdMapFromIdSet(insertionSetDiff, [new Y.Attribution('insert', 'Bob')]) const attributedDeletions = createIdMapFromIdSet(deleteSetDiff, [new Y.Attribution('delete', 'Bob')]) -// now we can define an attribution manager that maps these changes to output. One of the -// implementations is the TwosetAttributionManager -const attributionManager = new TwosetAttributionManager(attributedInsertions, attributedDeletions) -// we render the attributed content with the attributionManager -let attributedContent = ytext.toDelta(attributionManager) +// now we can define a renderer that maps these changes to output. One of the +// implementations is the TwosetRenderer +const renderer = new TwosetRenderer(attributedInsertions, attributedDeletions) +// we render the attributed content with the renderer +let attributedContent = ytext.toDelta({ renderer }) console.log(JSON.stringify(attributedContent.toJSON().ops, null, 2)) let expectedContent = delta.create().insert('Hell', { italic: true }, { attributes: { italic: ['Bob'] } }).insert('o ').insert('World', {}, { delete: ['Bob'] }).insert('attributions', {}, { insert: ['Bob'] }).insert('!') t.assert(attributedContent.equals(expectedContent)) @@ -113,9 +113,10 @@ const attributedDeletions = createIdMapFromIdSet(deleteSetDiff, [new Y.Attributi You could use the same output to calculate a real diff as well (consisting of deletions and insertions only, without Attributions). -`AttributionManager` is an abstract class for mapping attributions. It is -possible to highlight arbitrary content with this approach. +`AbstractRenderer` is the abstract base class for renderers, which map +attributions onto content. It is possible to highlight arbitrary content with +this approach. -The AttributionManager is encodes very efficiently. The ids are encoded using +The attribution data is encoded very efficiently. The ids are encoded using run-length encoding and the Attributes are de-duplicated and only encoded once. The above example encodes in 20 bytes. diff --git a/global.d.ts b/global.d.ts index 0c48c098..5694215b 100644 --- a/global.d.ts +++ b/global.d.ts @@ -46,8 +46,8 @@ declare type Delta = import('lib0/delta').Delta // @todo the below should have a separate Y/[mod] export declare type StackItem = import('./src/utils/UndoManager.js').StackItem declare type UndoManager = import('./src/utils/UndoManager.js').UndoManager -declare type AbstractAttributionManager = import('./src/utils/attribution-manager-helpers.js').AbstractAttributionManager -declare type Attribution = import('./src/utils/attribution-manager-helpers.js').Attribution -declare type AttributedContent = import('./src/utils/attribution-manager-helpers.js').AttributedContent +declare type AbstractRenderer = import('./src/utils/renderer-helpers.js').AbstractRenderer +declare type Attribution = import('./src/utils/renderer-helpers.js').Attribution +declare type AttributedContent = import('./src/utils/renderer-helpers.js').AttributedContent declare type Snapshot = import('./src/utils/Snapshot.js').Snapshot diff --git a/attribution-manager.md b/renderer.md similarity index 66% rename from attribution-manager.md rename to renderer.md index dc77553a..194538d3 100644 --- a/attribution-manager.md +++ b/renderer.md @@ -5,19 +5,19 @@ The Attribution feature extends Yjs types to provide rich metadata about content changes, including information about who created, deleted, or formatted content. This enables powerful collaborative editing features such as authorship tracking and change visualization. The information about who performed which changes can -be handled by a separate CRDT (which is part of the attribution manager). +be handled by a separate CRDT (which is part of the renderer). ## Core Concepts -### Attribution Manager +### Renderer -The `attributionManager` is the central component that tracks and manages -attribution data. It must be passed to methods that support attribution to -enable the feature. +A `renderer` renders Content (with its Attributions) to a delta. It is the +central component for the attribution feature: pass it to methods like +`toDelta()` / `getDelta()` to render content together with attribution metadata. -Different implementations of AttributionManager are available for different use cases: -- `DiffingAttributionManager`: Highlights the differences between two Yjs documents -- `SnapshotAttributionManager`: Highlights the differences between two snapshots +Different implementations of Renderer are available for different use cases: +- `DiffRenderer`: Highlights the differences between two Yjs documents +- `SnapshotRenderer`: Highlights the differences between two snapshots ### Attributed Content @@ -50,15 +50,15 @@ Deleted content is represented in attributed results to maintain authorship info ### YText -#### `getDelta([attributionManager])` +#### `getDelta([renderer])` Returns the delta representation of the YText content, optionally with attribution information. **Parameters:** -- `attributionManager` (optional): The attribution manager instance +- `renderer` (optional): The renderer instance **Returns:** -- Array of delta operations, with attribution metadata if `attributionManager` is provided +- Array of delta operations, with attribution metadata if `renderer` is provided **Examples:** @@ -72,46 +72,46 @@ const delta = ytext.getDelta() // [{ insert: 'hello world' }] // With attribution -const attributedDelta = ytext.getDelta(attributionManager) +const attributedDelta = ytext.getDelta({ renderer }) // [ // { insert: 'hello', attribution: { insert: ['kevin'] } }, // { insert: ' world', attribution: { insert: ['alice'] } } // ] ``` -#### `toDelta([attributionManager])` +#### `toDelta([renderer])` Returns the content representation with optional attribution information. **Parameters:** -- `toDelta` (optional): The attribution manager instance +- `renderer` (optional): The renderer instance **Returns:** -- Content representation with attribution metadata if `attributionManager` is provided +- Content representation with attribution metadata if `renderer` is provided ### YArray -#### `toDelta([attributionManager])` +#### `toDelta([renderer])` Returns the array content with optional attribution information for each element. **Parameters:** -- `attributionManager` (optional): The attribution manager instance +- `renderer` (optional): The renderer instance **Returns:** -- Array content with attribution metadata if `attributionManager` is provided +- Array content with attribution metadata if `renderer` is provided ### YMap -#### `toDelta([attributionManager])` +#### `toDelta([renderer])` Returns the map content with optional attribution information for each key-value pair. **Parameters:** -- `attributionManager` (optional): The attribution manager instance +- `renderer` (optional): The renderer instance **Returns:** -- Map content with attribution metadata if `attributionManager` is provided +- Map content with attribution metadata if `renderer` is provided ## Position Adjustments @@ -124,7 +124,7 @@ When working with attributed content, position calculations must account for del ytext.toString() // "world" // Attributed content (includes deleted content) -ytext.getDelta(attributionManager) +ytext.getDelta({ renderer }) // [ // { insert: 'hello ', attribution: { delete: ['kevin'] } }, // positions 0-5 // { insert: 'world' } // positions 6-10 @@ -141,7 +141,7 @@ Events in Yjs are enhanced to work with attributed content, automatically adjust ### Event Position Adjustment -When an `attributionManager` is used, event positions are automatically adjusted to account for deleted content. +When a `renderer` is used, event positions are automatically adjusted to account for deleted content. **Example:** @@ -157,8 +157,8 @@ ytext.observe((event, transaction) => { const standardDelta = event.getDelta() // Shows insertion at position 5 (after "world" in visible content) - // Attributed event (with attribution manager) - const attributedDelta = event.getDelta(attributionManager) + // Attributed event (with renderer) + const attributedDelta = event.getDelta({ renderer }) // Shows insertion at position 11 (accounting for deleted "hello ") // [ // { insert: 'hello ', attribution: { delete: ['kevin'] } }, @@ -175,8 +175,8 @@ ytext.observe((event, transaction) => { Display content with visual indicators of who created each part: ```javascript -function renderWithAuthorship(ytext, attributionManager) { - const attributedDelta = ytext.getDelta(attributionManager) +function renderWithAuthorship(ytext, renderer) { + const attributedDelta = ytext.getDelta({ renderer }) return attributedDelta.map(op => { const author = op.attribution?.insert?.[0] || 'unknown' @@ -197,9 +197,9 @@ function renderWithAuthorship(ytext, attributionManager) { Track who made specific changes to content: ```javascript -function trackChanges(ytext, attributionManager) { +function trackChanges(ytext, renderer) { ytext.observe((event, transaction) => { - const changes = event.changes.getAttributedDelta?.(attributionManager) || event.changes.delta + const changes = event.changes.getAttributedDelta?.(renderer) || event.changes.delta changes.forEach(change => { if (change.attribution) { @@ -212,11 +212,11 @@ function trackChanges(ytext, attributionManager) { ## Best Practices -### Attribution Manager Lifecycle +### Renderer Lifecycle -- Create one attribution manager per document or collaboration session -- Ensure the attribution manager is consistently used across all operations -- Pass the same attribution manager instance to all methods that need attribution +- Create one renderer per document or collaboration session +- Ensure the renderer is consistently used across all operations +- Pass the same renderer instance to all methods that need attribution ## Migration Guide @@ -224,14 +224,14 @@ function trackChanges(ytext, attributionManager) { To add attribution support to existing Yjs applications: -1. **Add attribution manager**: Create and configure an attribution manager -2. **Update method calls**: Add the attribution manager parameter to relevant method calls +1. **Add renderer**: Create and configure a renderer +2. **Update method calls**: Add the renderer parameter to relevant method calls 3. **Handle attributed content**: Update code to handle the new attribution metadata format 4. **Adjust position calculations**: Update position calculations to account for deleted content ### Backward Compatibility The Attribution feature is fully backward compatible: -- All existing methods work without the attribution manager parameter +- All existing methods work without the renderer parameter - Existing code continues to work unchanged - Attribution is opt-in and doesn't affect performance when not used diff --git a/src/index.js b/src/index.js index c1f95f23..faa7c9aa 100644 --- a/src/index.js +++ b/src/index.js @@ -18,7 +18,7 @@ export { Transaction, transact, cleanupYTextFormatting } from './utils/Transacti export { UndoManager, undoContentIds } from './utils/UndoManager.js' export { logUpdate, logUpdateV2, decodeUpdate, decodeUpdateV2, encodeStateVectorFromUpdate, encodeStateVectorFromUpdateV2, convertUpdateFormatV1ToV2, convertUpdateFormatV2ToV1, obfuscateUpdate, obfuscateUpdateV2, createContentIdsFromUpdate, createContentIdsFromUpdateV2, intersectUpdateWithContentIds, intersectUpdateWithContentIdsV2 } from './utils/updates.js' export { YEvent, getPathTo } from './utils/YEvent.js' -export { TwosetAttributionManager, noAttributionsManager, AbstractAttributionManager, createAttributionManagerFromDiff, DiffAttributionManager, createAttributionManagerFromSnapshots, SnapshotAttributionManager, Attributions, $attributionManager } from './utils/AttributionManager.js' +export { TwosetRenderer, baseRenderer, AbstractRenderer, createDiffRenderer, DiffRenderer, createSnapshotRenderer, SnapshotRenderer, Attributions, $renderer } from './utils/Renderer.js' export { diffDocsToDelta } from './utils/delta-helpers.js' export { YType as Type, getTypeChildren, typeMapGetSnapshot, typeMapGetAllSnapshot, $ytype, $ytypeAny } from './ytype.js' export { AbstractStruct } from './structs/AbstractStruct.js' diff --git a/src/utils/RelativePosition.js b/src/utils/RelativePosition.js index d51227e7..64b1722c 100644 --- a/src/utils/RelativePosition.js +++ b/src/utils/RelativePosition.js @@ -4,7 +4,7 @@ import * as error from 'lib0/error' import { Item, followRedone, ContentType } from '../structs/Item.js' import { writeID, readID, compareIDs, findRootTypeKey, createID } from './ID.js' -import { noAttributionsManager } from './attribution-manager-helpers.js' +import { baseRenderer } from './renderer-helpers.js' /** * A relative position is based on the Yjs model and is not affected by document changes. @@ -146,12 +146,12 @@ export const createRelativePosition = (type, item, assoc) => { * @param {YType} type The base type (e.g. YText or YArray). * @param {number} index The absolute position. * @param {number} [assoc] - * @param {import('../utils/AttributionManager.js').AbstractAttributionManager} attributionManager + * @param {import('../utils/Renderer.js').AbstractRenderer} renderer * @return {RelativePosition} * * @function */ -export const createRelativePositionFromTypeIndex = (type, index, assoc = 0, attributionManager = noAttributionsManager) => { +export const createRelativePositionFromTypeIndex = (type, index, assoc = 0, renderer = baseRenderer) => { let t = type._start if (assoc < 0) { // associated to the left character or the beginning of a type, increment index if possible. @@ -161,7 +161,7 @@ export const createRelativePositionFromTypeIndex = (type, index, assoc = 0, attr index-- } while (t !== null) { - const len = attributionManager.contentLength(t) + const len = renderer.contentLength(t) if (len > index) { // case 1: found position somewhere in the linked list return createRelativePosition(type, createID(t.id.client, t.id.clock + index), assoc) @@ -272,12 +272,12 @@ const getItemWithOffset = (store, id) => { * @param {RelativePosition} rpos * @param {Doc} doc * @param {boolean} followUndoneDeletions - whether to follow undone deletions - see https://github.com/yjs/yjs/issues/638 - * @param {import('../utils/AttributionManager.js').AbstractAttributionManager} attributionManager + * @param {import('../utils/Renderer.js').AbstractRenderer} renderer * @return {AbsolutePosition|null} * * @function */ -export const createAbsolutePositionFromRelativePosition = (rpos, doc, followUndoneDeletions = true, attributionManager = noAttributionsManager) => { +export const createAbsolutePositionFromRelativePosition = (rpos, doc, followUndoneDeletions = true, renderer = baseRenderer) => { const store = doc.store const rightID = rpos.item const typeID = rpos.type @@ -296,10 +296,10 @@ export const createAbsolutePositionFromRelativePosition = (rpos, doc, followUndo } type = /** @type {YType} */ (right.parent) if (type._item === null || !type._item.deleted) { - index = attributionManager.contentLength(right) === 0 ? 0 : (res.diff + (assoc >= 0 ? 0 : 1)) // adjust position based on left association if necessary + index = renderer.contentLength(right) === 0 ? 0 : (res.diff + (assoc >= 0 ? 0 : 1)) // adjust position based on left association if necessary let n = right.left while (n !== null) { - index += attributionManager.contentLength(n) + index += renderer.contentLength(n) n = n.left } } diff --git a/src/utils/AttributionManager.js b/src/utils/Renderer.js similarity index 91% rename from src/utils/AttributionManager.js rename to src/utils/Renderer.js index 9aeac711..229cbd32 100644 --- a/src/utils/AttributionManager.js +++ b/src/utils/Renderer.js @@ -11,16 +11,16 @@ import { UpdateEncoderV1 } from './UpdateEncoder.js' import { transact } from './Transaction.js' import { UndoManager, StackItem } from './UndoManager.js' -import { $attributionManager, AttributedContent } from './attribution-manager-helpers.js' +import { $renderer, AttributedContent } from './renderer-helpers.js' -export { noAttributionsManager, NoAttributionsManager, AbstractAttributionManager, $attributionManager } from './attribution-manager-helpers.js' +export { baseRenderer, BaseRenderer, AbstractRenderer, $renderer } from './renderer-helpers.js' /** - * @implements AbstractAttributionManager + * @implements AbstractRenderer * * @extends {ObservableV2<{change:(idset:IdSet,origin:any,local:boolean)=>void}>} */ -export class TwosetAttributionManager extends ObservableV2 { +export class TwosetRenderer extends ObservableV2 { /** * @param {IdMap} inserts * @param {IdMap} deletes @@ -31,7 +31,7 @@ export class TwosetAttributionManager extends ObservableV2 { this.deletes = deletes } - get $type () { return $attributionManager } + get $type () { return $renderer } /** * @param {Array>} contents - where to write the result @@ -93,15 +93,15 @@ const getItemContent = (store, client, clock, len) => { /** * @param {Transaction?} tr - only specify this if you want to fill the content of deleted content - * @param {DiffAttributionManager} am + * @param {DiffRenderer} renderer * @param {ID} start * @param {ID} end * @param {boolean} collectAll - collect as many items as possible. Accept adding redundant changes. */ -const collectSuggestedChanges = (tr, am, start, end, collectAll) => { +const collectSuggestedChanges = (tr, renderer, start, end, collectAll) => { const inserts = createIdSet() const deletes = createIdSet() - const store = am._nextDoc.store + const store = renderer._nextDoc.store /** * make sure to collect suggestions until all formats are closed * @type {Set} @@ -121,7 +121,7 @@ const collectSuggestedChanges = (tr, am, start, end, collectAll) => { break } if (!item.deleted) { - const slice = am.inserts.slice(item.id.client, item.id.clock, item.length) + const slice = renderer.inserts.slice(item.id.client, item.id.clock, item.length) if (slice.some(s => s.attrs === null)) { for (let i = slice.length - 1; i >= 0; i--) { const s = slice[i] @@ -137,7 +137,7 @@ const collectSuggestedChanges = (tr, am, start, end, collectAll) => { // eslint-disable-next-line itemLoop: while (item != null) { const itemClient = item.id.client - const slice = (item.deleted ? am.deletes : am.inserts).slice(itemClient, item.id.clock, item.length) + const slice = (item.deleted ? renderer.deletes : renderer.inserts).slice(itemClient, item.id.clock, item.length) foundEndItem ||= item === endItem if (item.deleted) { // item probably gc'd content. Need to split item and fill with content again @@ -154,7 +154,7 @@ const collectSuggestedChanges = (tr, am, start, end, collectAll) => { if (tr != null) { const splicedItem = getItemCleanStart(tr, createID(itemClient, s.clock)) if (s.attrs != null) { - splicedItem.content = getItemContent(am._prevDocStore, itemClient, s.clock, s.len) + splicedItem.content = getItemContent(renderer._prevDocStore, itemClient, s.clock, s.len) } } } @@ -197,15 +197,15 @@ export class Attributions { const extractAttributions = (attrs, slice) => attrs == null ? createIdMapFromIdSet(slice, []) : mergeIdMaps([intersectMaps(attrs, slice), createIdMapFromIdSet(slice, [])]) /** - * @implements AbstractAttributionManager + * @implements AbstractRenderer * * @extends {ObservableV2<{change:(idset:IdSet,origin:any,local:boolean)=>void}>} */ -export class DiffAttributionManager extends ObservableV2 { +export class DiffRenderer extends ObservableV2 { /** * @param {Doc} prevDoc * @param {Doc} nextDoc - * @param {Object} [options] - options for the attribution manager + * @param {Object} [options] - options for the renderer * @param {Attributions?} [options.attrs] - the attributes to apply to the diff */ constructor (prevDoc, nextDoc, { attrs = null } = {}) { @@ -264,7 +264,7 @@ export class DiffAttributionManager extends ObservableV2 { }) this._afterTrListener = nextDoc.on('afterTransaction', (tr) => { // apply deletes on attributed deletes (content that is already deleted, but is rendered by - // the attribution manager) + // the renderer) if (!this.suggestionMode && tr.local && (this.suggestionOrigins == null || this.suggestionOrigins.some(o => o === tr.origin))) { const attributedDeletes = tr.meta.get('attributedDeletes') if (attributedDeletes != null) { @@ -290,7 +290,7 @@ export class DiffAttributionManager extends ObservableV2 { prevDoc.on('destroy', this._destroyHandler) } - get $type () { return $attributionManager } + get $type () { return $renderer } destroy () { super.destroy() @@ -411,24 +411,24 @@ export class DiffAttributionManager extends ObservableV2 { * * @param {Doc} prevDoc * @param {Doc} nextDoc - * @param {Object} [options] - options for the attribution manager + * @param {Object} [options] - options for the renderer * @param {ContentMap?} [options.attrs] - the attributes to apply to the diff */ -export const createAttributionManagerFromDiff = (prevDoc, nextDoc, options) => new DiffAttributionManager(prevDoc, nextDoc, options) +export const createDiffRenderer = (prevDoc, nextDoc, options) => new DiffRenderer(prevDoc, nextDoc, options) /** - * Intended for projects that used the v13 snapshot feature. With this AttributionManager you can + * Intended for projects that used the v13 snapshot feature. With this renderer you can * read content similar to the previous snapshot api. Requires that `ydoc.gc` is turned off. * - * @implements AbstractAttributionManager + * @implements AbstractRenderer * * @extends {ObservableV2<{change:(idset:IdSet,origin:any,local:boolean)=>void}>} */ -export class SnapshotAttributionManager extends ObservableV2 { +export class SnapshotRenderer extends ObservableV2 { /** * @param {Snapshot} prevSnapshot * @param {Snapshot} nextSnapshot - * @param {Object} [options] - options for the attribution manager + * @param {Object} [options] - options for the renderer * @param {Array} [options.attrs] - the attributes to apply to the diff */ constructor (prevSnapshot, nextSnapshot) { @@ -445,7 +445,7 @@ export class SnapshotAttributionManager extends ObservableV2 { this.attrs = mergeIdMaps([diffIdMap(inserts, prevSnapshot.ds), deletes]) } - get $type () { return $attributionManager } + get $type () { return $renderer } /** * @param {Array>} contents - where to write the result @@ -495,4 +495,4 @@ export class SnapshotAttributionManager extends ObservableV2 { * @param {Snapshot} prevSnapshot * @param {Snapshot} nextSnapshot */ -export const createAttributionManagerFromSnapshots = (prevSnapshot, nextSnapshot = prevSnapshot) => new SnapshotAttributionManager(prevSnapshot, nextSnapshot) +export const createSnapshotRenderer = (prevSnapshot, nextSnapshot = prevSnapshot) => new SnapshotRenderer(prevSnapshot, nextSnapshot) diff --git a/src/utils/YEvent.js b/src/utils/YEvent.js index 45d441ad..8b404b18 100644 --- a/src/utils/YEvent.js +++ b/src/utils/YEvent.js @@ -2,7 +2,7 @@ import * as map from 'lib0/map' import * as set from 'lib0/set' import { diffIdSet, mergeIdSets } from './ids.js' -import { noAttributionsManager } from './attribution-manager-helpers.js' +import { baseRenderer } from './renderer-helpers.js' import { createAbsolutePositionFromRelativePosition, createRelativePosition } from './RelativePosition.js' /** @@ -85,14 +85,14 @@ export class YEvent { /** * @template {boolean} [Deep=false] - * @param {AbstractAttributionManager} am * @param {object} [opts] + * @param {AbstractRenderer} [opts.renderer] - renders the content (with attributions); defaults to `baseRenderer` * @param {Deep} [opts.deep] * @return {Deep extends true ? Delta : Delta>} The Delta representation of this type. * * @public */ - getDelta (am = noAttributionsManager, { deep } = {}) { + getDelta ({ renderer = baseRenderer, deep } = {}) { const itemsToRender = mergeIdSets([diffIdSet(this.transaction.insertSet, this.transaction.deleteSet), diffIdSet(this.transaction.deleteSet, this.transaction.insertSet)]) /** * @todo this should be done only one in the transaction step @@ -120,7 +120,7 @@ export class YEvent { } modified = dchanged } - return /** @type {any} */ (this.target.toDelta(am, { itemsToRender, retainDeletes: true, deletedItems: this.transaction.deleteSet, deep: !!deep, modified })) + return /** @type {any} */ (this.target.toDelta({ renderer, itemsToRender, retainDeletes: true, deletedItems: this.transaction.deleteSet, deep: !!deep, modified })) } /** @@ -142,7 +142,7 @@ export class YEvent { * @public */ get deltaDeep () { - return /** @type {any} */ (this._deltaDeep ?? (this._deltaDeep = /** @type {any} */ (this.getDelta(noAttributionsManager, { deep: true })))) + return /** @type {any} */ (this._deltaDeep ?? (this._deltaDeep = /** @type {any} */ (this.getDelta({ deep: true })))) } } @@ -158,13 +158,13 @@ export class YEvent { * * @param {YType} parent * @param {YType} child target - * @param {AbstractAttributionManager} am + * @param {AbstractRenderer} renderer * @return {Array} Path to the target * * @private * @function */ -export const getPathTo = (parent, child, am = noAttributionsManager) => { +export const getPathTo = (parent, child, renderer = baseRenderer) => { const path = [] const doc = /** @type {Doc} */ (parent.doc) while (child._item !== null && child !== parent) { @@ -174,7 +174,7 @@ export const getPathTo = (parent, child, am = noAttributionsManager) => { } else { const parent = /** @type {import('../ytype.js').YType} */ (child._item.parent) // parent is array-ish - const apos = /** @type {import('../utils/RelativePosition.js').AbsolutePosition} */ (createAbsolutePositionFromRelativePosition(createRelativePosition(parent, child._item.id), doc, false, am)) + const apos = /** @type {import('../utils/RelativePosition.js').AbsolutePosition} */ (createAbsolutePositionFromRelativePosition(createRelativePosition(parent, child._item.id), doc, false, renderer)) path.unshift(apos.index) } child = /** @type {YType} */ (child._item.parent) diff --git a/src/utils/delta-helpers.js b/src/utils/delta-helpers.js index 241f9dda..359aea76 100644 --- a/src/utils/delta-helpers.js +++ b/src/utils/delta-helpers.js @@ -1,6 +1,6 @@ import * as delta from 'lib0/delta' import { createInsertSetFromStructStore, createDeleteSetFromStructStore, diffIdSet, mergeIdSets } from './ids.js' -import { createAttributionManagerFromDiff } from './AttributionManager.js' +import { createDiffRenderer } from './Renderer.js' import { computeModifiedFromItems } from '../ytype.js' /** @@ -8,7 +8,7 @@ import { computeModifiedFromItems } from '../ytype.js' * @param {Doc} v2 * @return {delta.DeltaBuilderAny} */ -export const diffDocsToDelta = (v1, v2, { am = createAttributionManagerFromDiff(v1, v2) } = {}) => { +export const diffDocsToDelta = (v1, v2, { renderer = createDiffRenderer(v1, v2) } = {}) => { const insertDiff = diffIdSet(createInsertSetFromStructStore(v2.store, false), createInsertSetFromStructStore(v1.store, false)) const deleteDiff = diffIdSet(createDeleteSetFromStructStore(v2.store), createDeleteSetFromStructStore(v1.store)) // don't render items that have been inserted and then deleted @@ -23,8 +23,8 @@ export const diffDocsToDelta = (v1, v2, { am = createAttributionManagerFromDiff( v2.share.forEach((type, typename) => { const typeConf = changedTypes.get(type) if (typeConf) { - const shareDelta = type.toDelta(am, { - itemsToRender, retainDeletes: true, deletedItems: deletesOnly, modified: changedTypes, deep: true + const shareDelta = type.toDelta({ + renderer, itemsToRender, retainDeletes: true, deletedItems: deletesOnly, modified: changedTypes, deep: true }) d.modifyAttr(typename, shareDelta) } diff --git a/src/utils/attribution-manager-helpers.js b/src/utils/renderer-helpers.js similarity index 84% rename from src/utils/attribution-manager-helpers.js rename to src/utils/renderer-helpers.js index 816074f5..33e90584 100644 --- a/src/utils/attribution-manager-helpers.js +++ b/src/utils/renderer-helpers.js @@ -38,14 +38,14 @@ export class AttributedContent { } /** - * Abstract class for associating Attributions to content / changes + * Abstract base class for renderers. A renderer renders Content (with Attributions) to a delta. * * Should fire an event when the attributions changed _after_ the original change happens. This * Event will be used to update the attribution on the current content. * * @extends {ObservableV2<{change:(idset:IdSet,origin:any,local:boolean)=>void}>} */ -export class AbstractAttributionManager extends ObservableV2 { +export class AbstractRenderer extends ObservableV2 { /** * @param {Array>} _contents - where to write the result * @param {number} _client @@ -72,17 +72,17 @@ export class AbstractAttributionManager extends ObservableV2 { } } -export const $attributionManager = AbstractAttributionManager.prototype.$type = s.$type('y:am', AbstractAttributionManager) +export const $renderer = AbstractRenderer.prototype.$type = s.$type('y:r', AbstractRenderer) /** - * Abstract class for associating Attributions to content / changes + * The default renderer. Renders content as-is, without looking up any attributions. * - * @implements AbstractAttributionManager + * @implements AbstractRenderer * * @extends {ObservableV2<{change:(idset:IdSet,origin:any,local:boolean)=>void}>} */ -export class NoAttributionsManager extends ObservableV2 { - get $type () { return $attributionManager } +export class BaseRenderer extends ObservableV2 { + get $type () { return $renderer } /** * @param {Array>} contents - where to write the result @@ -107,4 +107,4 @@ export class NoAttributionsManager extends ObservableV2 { } } -export const noAttributionsManager = new NoAttributionsManager() +export const baseRenderer = new BaseRenderer() diff --git a/src/ytype.js b/src/ytype.js index bad51b3c..90dc8146 100644 --- a/src/ytype.js +++ b/src/ytype.js @@ -26,7 +26,7 @@ import { ContentDoc, createContentDocFromDoc } from './structs/Item.js' -import { noAttributionsManager } from './utils/attribution-manager-helpers.js' +import { baseRenderer } from './utils/renderer-helpers.js' import { removeEventHandlerListener, callEventHandlerListeners, addEventHandlerListener, createEventHandler } from './utils/EventHandler.js' import { createID } from './utils/ID.js' import { createIdSet, iterateStructsByIdSetWithoutSplits } from './utils/ids.js' @@ -99,14 +99,14 @@ export class ItemTextListPosition { * @param {Item|null} right * @param {number} index * @param {Map} currentAttributes - * @param {AbstractAttributionManager} am + * @param {AbstractRenderer} renderer */ - constructor (left, right, index, currentAttributes, am) { + constructor (left, right, index, currentAttributes, renderer) { this.left = left this.right = right this.index = index this.currentAttributes = currentAttributes - this.am = am + this.renderer = renderer } /** @@ -123,7 +123,7 @@ export class ItemTextListPosition { } break default: - this.index += this.am.contentLength(this.right) + this.index += this.renderer.contentLength(this.right) break } this.left = this.right @@ -150,7 +150,7 @@ export class ItemTextListPosition { (length > 0 || ( negatedAttributes.size > 0 && - ((this.right.deleted && this.am.contentLength(this.right) === 0) || this.right.content.constructor === ContentFormat) + ((this.right.deleted && this.renderer.contentLength(this.right) === 0) || this.right.content.constructor === ContentFormat) ) ) ) { @@ -179,13 +179,13 @@ export class ItemTextListPosition { } default: { const item = this.right - const rightLen = this.am.contentLength(item) + const rightLen = this.renderer.contentLength(item) if (length < rightLen) { /** * @type {Array>} */ const contents = [] - this.am.readContent(contents, item.id.client, item.id.clock, item.deleted, item.content, 0) + this.renderer.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] @@ -227,7 +227,7 @@ 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.deleted && (currPos.renderer === baseRenderer || currPos.renderer.contentLength(currPos.right) === 0)) || ( currPos.right.content.constructor === ContentFormat && equalAttrs(negatedAttributes.get(/** @type {ContentFormat} */ (currPos.right.content).key), /** @type {ContentFormat} */ (currPos.right.content).value) ) @@ -278,7 +278,7 @@ const minimizeAttributeChanges = (currPos, attributes) => { 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 if (currPos.right.deleted ? (currPos.renderer.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 @@ -402,12 +402,12 @@ export const deleteText = (transaction, currPos, length) => { } length -= item.length item.delete(transaction) - } else if (currPos.am !== noAttributionsManager) { + } else if (currPos.renderer !== baseRenderer) { /** * @type {Array>} */ const contents = [] - currPos.am.readContent(contents, item.id.client, item.id.clock, true, item.content, 0) + currPos.renderer.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) { @@ -820,8 +820,8 @@ export class YType { * * @template {boolean} [Deep=false] * - * @param {AbstractAttributionManager} am * @param {Object} [opts] + * @param {AbstractRenderer} [opts.renderer] - renders the content (with attributions); defaults to `baseRenderer` * @param {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 @@ -832,8 +832,8 @@ export class YType { * * @public */ - toDelta (am = noAttributionsManager, opts = {}) { - const { itemsToRender = null, retainInserts = false, retainDeletes = false, deletedItems = null, deep = false } = opts + toDelta (opts = {}) { + const { renderer = baseRenderer, itemsToRender = null, retainInserts = false, retainDeletes = false, deletedItems = null, deep = false } = opts const { modified = (deep && itemsToRender) ? computeModifiedFromItems(/** @type {Doc} */ (this.doc).store, itemsToRender) : null } = opts const renderAttrs = modified?.get(this) || null const renderChildren = modified == null || !modified.has(this) || /** @type {Set} */ (modified.get(this)).has(null) @@ -841,9 +841,9 @@ export class YType { * @type {delta.DeltaBuilderAny} */ const d = /** @type {any} */ (delta.create(this.name)) - const optsAll = object.assign({}, opts, { modified }) + const optsAll = object.assign({}, opts, { renderer, modified }) // opts has been re-computed - do not use opts after this point! - typeMapGetDelta(d, /** @type {any} */ (this), renderAttrs, am, deep, modified, deletedItems, itemsToRender, optsAll, optsAll) + typeMapGetDelta(d, /** @type {any} */ (this), renderAttrs, renderer, deep, modified, deletedItems, itemsToRender, optsAll, optsAll) if (renderChildren) { /** * @type {delta.FormattingAttributes} @@ -884,12 +884,12 @@ export class YType { 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) + renderer.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) + renderer.readContent(cs, item.id.client, item.id.clock, item.deleted, item.content, 1) } } for (let i = 0; i < cs.length; i++) { @@ -935,12 +935,12 @@ export class YType { if (c.deleted ? retainDeletes : retainInserts) { if (c.deleted && c.content.constructor === ContentType) { // @todo use current transaction instead - d.modify(/** @type {any} */ (c.content).type.toDelta(am, optsAll), null, attribution ?? {}) + d.modify(/** @type {any} */ (c.content).type.toDelta(optsAll), null, attribution ?? {}) } else { d.retain(c.content.getLength(), null, attribution ?? {}) } } else if (deep && c.content.constructor === ContentType) { - d.insert([/** @type {any} */(c.content).type.toDelta(am, optsAll)], null, attribution) + d.insert([/** @type {any} */(c.content).type.toDelta(optsAll)], null, attribution) } else { d.insert(c.content.getContent(), null, attribution) } @@ -949,7 +949,7 @@ export class YType { } 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.toDelta(am, optsAll)) + d.modify(/** @type {any} */ (c.content).type.toDelta(optsAll)) } else { d.usedAttributes = changedAttributes usingChangedAttributes = true @@ -1060,28 +1060,30 @@ export class YType { * Render the difference to another ydoc (which can be empty) and highlight the differences with * attributions. * - * @param {AbstractAttributionManager} am + * @param {Object} [opts] + * @param {AbstractRenderer} [opts.renderer] - renders the content (with attributions); defaults to `baseRenderer` * @return {delta.Delta} */ - toDeltaDeep (am = noAttributionsManager) { - return /** @type {any} */ (this.toDelta(am, { deep: true })) + toDeltaDeep (opts = {}) { + return /** @type {any} */ (this.toDelta({ ...opts, deep: true })) } /** * Apply a {@link Delta} on this shared type. * * @param {delta.DeltaAny} d The changes to apply on this element. - * @param {AbstractAttributionManager} am + * @param {Object} [opts] + * @param {AbstractRenderer} [opts.renderer] - renders the content (with attributions); defaults to `baseRenderer` * * @public */ - applyDelta (d, am = noAttributionsManager) { + applyDelta (d, { renderer = baseRenderer } = {}) { if (this.doc == null) { (this._prelim || (this._prelim = /** @type {any} */ (delta.create()))).apply(d) } else if (this._item?.deleted !== true) { // @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) + const currPos = new ItemTextListPosition(null, this._start, 0, new Map(), renderer) for (const op of d.children) { if (delta.$textOp.check(op)) { insertContent(transaction, /** @type {any} */ (this), currPos, new ContentString(op.insert), op.format || {}) @@ -1095,7 +1097,7 @@ export class YType { let item = currPos.right while (item != null && (item.deleted || !item.countable)) { item = item.next } if (item == null || item.content.constructor !== ContentType) { error.unexpectedCase() } - /** @type {ContentType} */ (item.content).type.applyDelta(op.value, am) + /** @type {ContentType} */ (item.content).type.applyDelta(op.value, { renderer }) currPos.formatText(transaction, /** @type {any} */ (this), 1, op.format || {}) } else { error.unexpectedCase() @@ -1111,7 +1113,7 @@ export class YType { if (!(sub instanceof YType)) { error.unexpectedCase() } - sub.applyDelta(op.value, am) + sub.applyDelta(op.value, { renderer }) } } }) @@ -1897,7 +1899,7 @@ export const typeMapGetAll = (parent) => { * @param {TypeDelta} d * @param {YType} parent * @param {Set?} attrsToRender - * @param {AbstractAttributionManager} am + * @param {AbstractRenderer} renderer * @param {boolean} deep * @param {Set|Map|null} [modified] - set of types that should be rendered as modified children * @param {IdSet?} [deletedItems] @@ -1908,7 +1910,7 @@ export const typeMapGetAll = (parent) => { * @private * @function */ -export const typeMapGetDelta = (d, parent, attrsToRender, am, deep, modified, deletedItems, itemsToRender, opts, optsAll) => { +export const typeMapGetDelta = (d, parent, attrsToRender, renderer, deep, modified, deletedItems, itemsToRender, opts, optsAll) => { // @todo support modified ops! /** * @param {Item} item @@ -1919,7 +1921,7 @@ export const typeMapGetDelta = (d, parent, attrsToRender, am, deep, modified, de * @type {Array} */ const cs = [] - am.readContent(cs, item.id.client, item.id.clock, item.deleted, item.content, 1) + renderer.readContent(cs, item.id.client, item.id.clock, item.deleted, item.content, 1) const { deleted, attrs, content, render } = cs[cs.length - 1] if (!render) return const attribution = createAttributionFromAttributionItems(attrs, deleted) @@ -1941,17 +1943,17 @@ export const typeMapGetDelta = (d, parent, attrsToRender, am, deep, modified, de } } } else if (deep && c instanceof YType && modified?.has(c)) { - d.modifyAttr(key, c.toDelta(am, opts)) + d.modifyAttr(key, c.toDelta(opts)) } else { // find prev content let prevContentItem = item - // this algorithm is problematic. should check all previous content using am.readcontent + // this algorithm is problematic. should check all previous content using renderer.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 YType) { - c = /** @type {any} */(c).toDelta(am, optsAll) + c = /** @type {any} */(c).toDelta(optsAll) } d.setAttr(key, c, attribution, prevValue) } diff --git a/tests/attribution.tests.js b/tests/attribution.tests.js index d448cc50..79e481c4 100644 --- a/tests/attribution.tests.js +++ b/tests/attribution.tests.js @@ -20,9 +20,9 @@ export const testRelativePositions = _tc => { const v1 = Y.cloneDoc(ydoc) ytext.delete(1, 6) ytext.insert(1, 'x') - const am = Y.createAttributionManagerFromDiff(v1, ydoc) - const rel = Y.createRelativePositionFromTypeIndex(ytext, 9, 1, am) // pos after "hello wo" - const abs1 = Y.createAbsolutePositionFromRelativePosition(rel, ydoc, true, am) + const renderer = Y.createDiffRenderer(v1, ydoc) + const rel = Y.createRelativePositionFromTypeIndex(ytext, 9, 1, renderer) // pos after "hello wo" + const abs1 = Y.createAbsolutePositionFromRelativePosition(rel, ydoc, true, renderer) const abs2 = Y.createAbsolutePositionFromRelativePosition(rel, ydoc, true) t.assert(abs1?.index === 9) t.assert(abs2?.index === 3) @@ -39,16 +39,16 @@ export const testAttributedEvents = _tc => { ydoc.transact(() => { ytext.delete(6, 5) }) - const am = Y.createAttributionManagerFromDiff(v1, ydoc) - const c1 = ytext.toDelta(am) + const renderer = Y.createDiffRenderer(v1, ydoc) + const c1 = ytext.toDelta({ renderer }) t.compare(c1, delta.create().insert('hello ').insert('world', null, { delete: [] }).done()) let calledObserver = false ytext.observe(event => { - const d = event.getDelta(am) + const d = event.getDelta({ renderer }) t.compare(d, delta.create().retain(11).insert('!', null, { insert: [] }).done()) calledObserver = true }) - ytext.applyDelta(delta.create().retain(11).insert('!').done(), am) + ytext.applyDelta(delta.create().retain(11).insert('!').done(), { renderer }) t.assert(calledObserver) } @@ -63,10 +63,10 @@ export const testInsertionsMindingAttributedContent = _tc => { ydoc.transact(() => { ytext.delete(6, 5) }) - const am = Y.createAttributionManagerFromDiff(v1, ydoc) - const c1 = ytext.toDelta(am) + const renderer = Y.createDiffRenderer(v1, ydoc) + const c1 = ytext.toDelta({ renderer }) t.compare(c1, delta.create().insert('hello ').insert('world', null, { delete: [] }).done()) - ytext.applyDelta(delta.create().retain(11).insert('content').done(), am) + ytext.applyDelta(delta.create().retain(11).insert('content').done(), { renderer }) t.assert(ytext.toString() === 'hello content') } @@ -81,10 +81,10 @@ export const testInsertionsIntoAttributedContent = _tc => { ydoc.transact(() => { ytext.insert(6, 'word') }) - const am = Y.createAttributionManagerFromDiff(v1, ydoc) - const c1 = ytext.toDelta(am) + const renderer = Y.createDiffRenderer(v1, ydoc) + const c1 = ytext.toDelta({ renderer }) t.compare(c1, delta.create().insert('hello ').insert('word', null, { insert: [] }).done()) - ytext.applyDelta(delta.create().retain(9).insert('l').done(), am) + ytext.applyDelta(delta.create().retain(9).insert('l').done(), { renderer }) t.assert(ytext.toString() === 'hello world') } @@ -151,21 +151,21 @@ export const testAttributionSession1 = tc => { text0.insert(0, 'a') text1.insert(0, 'b') testConnector.flushAllMessages() - const d1 = text0.toDelta(Y.createAttributionManagerFromDiff(v1, users[0], { attrs: globalAttributions })) + const d1 = text0.toDelta({ renderer: Y.createDiffRenderer(v1, users[0], { attrs: globalAttributions }) }) t.compare(d1, delta.create().insert('a', null, { insert: ['0'] }).insert('b', null, { insert: ['1'] }).done()) const v2 = Y.cloneDoc(users[0]) text0.delete(1, 1) text1.insert(2, 'c') testConnector.flushAllMessages() - const d2 = text0.toDelta(Y.createAttributionManagerFromDiff(v2, users[0], { attrs: globalAttributions })) + const d2 = text0.toDelta({ renderer: Y.createDiffRenderer(v2, users[0], { attrs: globalAttributions }) }) t.compare(d2, delta.create().insert('a').insert('b', null, { delete: ['0'] }).insert('c', null, { insert: ['1'] }).done()) const onlyUser0ChangesAttributed = { inserts: Y.filterIdMap(globalAttributions.inserts, attrs => attrs.some(attr => attr.name === 'insert' && attr.val === '0')), deletes: Y.filterIdMap(globalAttributions.deletes, attrs => attrs.some(attr => attr.name === 'delete' && attr.val === '0')) } - const amUser0 = new Y.TwosetAttributionManager(onlyUser0ChangesAttributed.inserts, onlyUser0ChangesAttributed.deletes) - const d3 = text0.toDelta(amUser0) + const rendererUser0 = new Y.TwosetRenderer(onlyUser0ChangesAttributed.inserts, onlyUser0ChangesAttributed.deletes) + const d3 = text0.toDelta({ renderer: rendererUser0 }) t.compare(d3, delta.create().insert('a', null, { insert: ['0'] }).insert('b', null, { delete: ['0'] }).insert('c').done()) Y.undoContentIds(users[0], Y.createContentIdsFromContentMap(onlyUser0ChangesAttributed)) @@ -179,10 +179,10 @@ export const testAttributionEvent = () => { //

hi

ytype.applyDelta(delta.create().insert([delta.create('p').insert('hi').done()]).done()) const ydocBase = Y.cloneDoc(ydoc) - const am = Y.createAttributionManagerFromDiff(ydocBase, ydoc) + const renderer = Y.createDiffRenderer(ydocBase, ydoc) let called = false ytype.observeDeep(event => { - const change = event.getDelta(am) + const change = event.getDelta({ renderer }) const expectedChange = delta.create().modify(delta.create('p').retain(2, null, { delete: [] }), null, { delete: [] }).done() t.compare( change, @@ -201,12 +201,12 @@ export const testAttributionChange = () => { const ytype = ydoc.get() ytype.applyDelta(delta.create().insert('hi').done()) const ydocClone = Y.cloneDoc(ydoc) - const am = Y.createAttributionManagerFromDiff(ydocClone, ydoc) + const renderer = Y.createDiffRenderer(ydocClone, ydoc) ytype.applyDelta(delta.create().retain(2).insert('!').done()) let calledHandler = false - am.on('change', changes => { + renderer.on('change', changes => { calledHandler = true - const changeUpdate = ytype.toDelta(am, { deep: true, itemsToRender: changes, retainInserts: true, retainDeletes: true }) + const changeUpdate = ytype.toDelta({ renderer, deep: true, itemsToRender: changes, retainInserts: true, retainDeletes: true }) const expectedUpdate = delta.create().retain(2).retain(1, null, {}) t.compare(changeUpdate, expectedUpdate) console.log(changeUpdate.toJSON()) diff --git a/tests/delta.tests.js b/tests/delta.tests.js index 9da0da98..f802d67e 100644 --- a/tests/delta.tests.js +++ b/tests/delta.tests.js @@ -168,14 +168,14 @@ export const testAttributions = _tc => { const ytype = ydoc.get('txt') // delete " world" and insert exclamation mark "!". ytype.applyDelta(delta.create().retain(5).delete(6).insert('!').done()) - const am = Y.createAttributionManagerFromDiff(ydocV1, ydoc) + const renderer = Y.createDiffRenderer(ydocV1, ydoc) // get the attributed differences - const attributedContent = ytype.toDelta(am) + const attributedContent = ytype.toDelta({ renderer }) console.log('attributed content', attributedContent.toJSON()) t.assert(attributedContent.equals(delta.create().insert('hello').insert(' world', null, { delete: [] }).insert('!', null, { insert: [] }).done())) // for editor bindings, it is also necessary to observe changes and get the attributed changes ytype.observe(event => { - const attributedChange = event.getDelta(am) + const attributedChange = event.getDelta({ renderer }) console.log('the attributed change', attributedChange.toJSON()) t.assert(attributedChange.done().equals(delta.create().retain(11).insert('!', null, { insert: [] }).done())) const unattributedChange = event.delta @@ -191,7 +191,7 @@ export const testAttributions = _tc => { * UNattributed: 'world!' */ // Apply a change to the attributed content - ytype.applyDelta(delta.create().retain(11).insert('!').done(), am) + ytype.applyDelta(delta.create().retain(11).insert('!').done(), { renderer }) // // Equivalent to applying a change to the UNattributed content: // ytype.applyDelta(delta.create().retain(5).insert('!')) } diff --git a/tests/y-array.tests.js b/tests/y-array.tests.js index dc87491e..f7d678c8 100644 --- a/tests/y-array.tests.js +++ b/tests/y-array.tests.js @@ -467,11 +467,11 @@ export const testAttributedContent = _tc => { */ const yarray = ydoc.get() yarray.insert(0, [1, 2]) - let attributionManager = Y.noAttributionsManager + let renderer = Y.baseRenderer ydoc.on('afterTransaction', tr => { - // attributionManager = new TwosetAttributionManager(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertAt', 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, [])) + // renderer = new TwosetRenderer(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertAt', 42), new Y.Attribution('insert', 'kevin')]), createIdMapFromIdSet(tr.deleteSet, [new Y.Attribution('delete', 'kevin')])) + renderer = new Y.TwosetRenderer(Y.createIdMapFromIdSet(tr.insertSet, []), Y.createIdMapFromIdSet(tr.deleteSet, [])) }) t.group('insert / delete', () => { ydoc.transact(() => { @@ -479,7 +479,7 @@ export const testAttributedContent = _tc => { yarray.insert(1, [42]) }) const expectedContent = delta.create().insert([1], null, { delete: [] }).insert([2]).insert([42], null, { insert: [] }) - const attributedContent = yarray.toDelta(attributionManager) + const attributedContent = yarray.toDelta({ renderer }) console.log(attributedContent.toJSON()) t.assert(attributedContent.equals(expectedContent)) }) diff --git a/tests/y-map.tests.js b/tests/y-map.tests.js index 70ba4200..59c3d351 100644 --- a/tests/y-map.tests.js +++ b/tests/y-map.tests.js @@ -1,6 +1,6 @@ import * as Y from '../src/index.js' import { init, compare, applyRandomTests, Doc } from './testHelper.js' // eslint-disable-line -import { noAttributionsManager, TwosetAttributionManager } from '../src/utils/AttributionManager.js' +import { baseRenderer, TwosetRenderer } from '../src/utils/Renderer.js' import { createIdMapFromIdSet } from '../src/utils/ids.js' import * as t from 'lib0/testing' import * as prng from 'lib0/prng' @@ -550,35 +550,35 @@ export const testYmapEventHasCorrectValueWhenSettingAPrimitiveFromOtherUser = tc export const testAttributedContent = _tc => { const ydoc = new Y.Doc({ gc: false }) const ymap = ydoc.get() - let attributionManager = noAttributionsManager + let renderer = baseRenderer ydoc.on('afterTransaction', tr => { - // attributionManager = new TwosetAttributionManager(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertAt', 42), new Y.Attribution('insert', 'kevin')]), createIdMapFromIdSet(tr.deleteSet, [new Y.Attribution('delete', 'kevin')])) - attributionManager = new TwosetAttributionManager(createIdMapFromIdSet(tr.insertSet, []), createIdMapFromIdSet(tr.deleteSet, [])) + // renderer = new TwosetRenderer(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertAt', 42), new Y.Attribution('insert', 'kevin')]), createIdMapFromIdSet(tr.deleteSet, [new Y.Attribution('delete', 'kevin')])) + renderer = new TwosetRenderer(createIdMapFromIdSet(tr.insertSet, []), createIdMapFromIdSet(tr.deleteSet, [])) }) t.group('initial value', () => { ymap.setAttr('test', 42) const expectedContent = { test: delta.$deltaMapChangeJson.expect({ type: 'insert', value: 42, attribution: { insert: [] } }) } - const attributedContent = ymap.toDelta(attributionManager) + const attributedContent = ymap.toDelta({ renderer }) console.log(attributedContent.toJSON()) t.compare(expectedContent, attributedContent.toJSON().attrs) }) t.group('overwrite value', () => { ymap.setAttr('test', 'fourtytwo') const expectedContent = { test: delta.$deltaMapChangeJson.expect({ type: 'insert', value: 'fourtytwo', attribution: { insert: [] } }) } - const attributedContent = ymap.toDelta(attributionManager) + const attributedContent = ymap.toDelta({ renderer }) console.log(attributedContent) t.compare(expectedContent, attributedContent.toJSON().attrs) }) t.group('delete value', () => { ymap.deleteAttr('test') - // Snapshot-mode `toDelta(am)` (no `itemsToRender` opt) must not emit + // Snapshot-mode `toDelta(renderer)` (no `itemsToRender` opt) must not emit // `DeleteAttrOp`. An attribute deleted under attribution is still // observable in the rendered state with its prior value and a `delete` // attribution marker - symmetric with how soft-deleted content children // surface as `InsertOp` with `{ delete: [] }` rather than `DeleteOp`. const expectedContent = { test: delta.$deltaMapChangeJson.expect({ type: 'insert', value: 'fourtytwo', attribution: { delete: [] } }) } - const attributedContent = ymap.toDelta(attributionManager) + const attributedContent = ymap.toDelta({ renderer }) console.log(attributedContent.toJSON()) t.compare(expectedContent, attributedContent.toJSON().attrs) }) diff --git a/tests/y-text.tests.js b/tests/y-text.tests.js index 20dffe12..8a3441fa 100644 --- a/tests/y-text.tests.js +++ b/tests/y-text.tests.js @@ -4,7 +4,7 @@ import * as prng from 'lib0/prng' import * as math from 'lib0/math' import * as delta from 'lib0/delta' import { createIdMapFromIdSet } from '../src/utils/ids.js' -import { noAttributionsManager, TwosetAttributionManager, createAttributionManagerFromSnapshots } from '../src/utils/AttributionManager.js' +import { baseRenderer, TwosetRenderer, createSnapshotRenderer } from '../src/utils/Renderer.js' const { init, compare } = Y @@ -1476,11 +1476,11 @@ export const testSnapshot = tc => { .insert('x') .delete(1) ) - const state1 = text0.toDelta(createAttributionManagerFromSnapshots(snapshot1)) + const state1 = text0.toDelta({ renderer: createSnapshotRenderer(snapshot1) }) t.compare(state1, delta.create().insert('abcd').done()) - const state2 = text0.toDelta(createAttributionManagerFromSnapshots(snapshot2)) + const state2 = text0.toDelta({ renderer: createSnapshotRenderer(snapshot2) }) t.compare(state2, delta.create().insert('axcd').done()) - const state2Diff = text0.toDelta(createAttributionManagerFromSnapshots(snapshot1, snapshot2)) + const state2Diff = text0.toDelta({ renderer: createSnapshotRenderer(snapshot1, snapshot2) }) t.compare( state2Diff, delta.create() @@ -1509,7 +1509,7 @@ export const testSnapshotDeleteAfter = tc => { .insert('e') .done() ) - const state1 = text0.toDelta(createAttributionManagerFromSnapshots(snapshot1)) + const state1 = text0.toDelta({ renderer: createSnapshotRenderer(snapshot1) }) t.compare(state1, delta.create().insert('abcd').done()) } @@ -1900,23 +1900,23 @@ export const testAttributedContent = _tc => { const ydoc = new Y.Doc({ gc: false }) const ytext = ydoc.get() ytext.insert(0, 'Hello World!') - let attributionManager = noAttributionsManager + let renderer = baseRenderer ydoc.on('afterTransaction', tr => { - // attributionManager = new TwosetAttributionManager(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertAt', 42), new Y.Attribution('insert', 'kevin')]), createIdMapFromIdSet(tr.deleteSet, [new Y.Attribution('delete', 'kevin')])) - attributionManager = new TwosetAttributionManager(createIdMapFromIdSet(tr.insertSet, []), createIdMapFromIdSet(tr.deleteSet, [])) + // renderer = new TwosetRenderer(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertAt', 42), new Y.Attribution('insert', 'kevin')]), createIdMapFromIdSet(tr.deleteSet, [new Y.Attribution('delete', 'kevin')])) + renderer = new TwosetRenderer(createIdMapFromIdSet(tr.insertSet, []), createIdMapFromIdSet(tr.deleteSet, [])) }) t.group('insert / delete / format', () => { ytext.applyDelta(delta.create().retain(4, { italic: true }).retain(2).delete(5).insert('attributions').done()) const expectedContent = delta.create().insert('Hell', { italic: true }, { format: { italic: [] } }).insert('o ').insert('World', {}, { delete: [] }).insert('attributions', {}, { insert: [] }).insert('!') - const attributedContent = ytext.toDelta(attributionManager) + const attributedContent = ytext.toDelta({ renderer }) console.log(attributedContent.toJSON()) t.assert(attributedContent.equals(expectedContent)) }) t.group('unformat', () => { ytext.applyDelta(delta.create().retain(5, { italic: null })) const expectedContent = delta.create().insert('Hell', null, { format: { italic: [] } }).insert('o attributions!') - const attributedContent = ytext.toDelta(attributionManager) + const attributedContent = ytext.toDelta({ renderer }) console.log(attributedContent.toJSON()) t.assert(attributedContent.equals(expectedContent)) }) @@ -1944,10 +1944,10 @@ export const testAttributedDiffing = _tc => { const attributedInsertions = createIdMapFromIdSet(insertionSetDiff, [Y.createContentAttribute('insert', 'Bob')]) const attributedDeletions = createIdMapFromIdSet(deleteSetDiff, [Y.createContentAttribute('delete', 'Bob')]) // now we can define an attribution manager that maps these changes to output. One of the - // implementations is the TwosetAttributionManager - const attributionManager = new TwosetAttributionManager(attributedInsertions, attributedDeletions) - // we render the attributed content with the attributionManager - const attributedContent = ytext.toDelta(attributionManager) + // implementations is the TwosetRenderer + const renderer = new TwosetRenderer(attributedInsertions, attributedDeletions) + // we render the attributed content with the renderer + const attributedContent = ytext.toDelta({ renderer }) console.log(JSON.stringify(attributedContent.toJSON(), null, 2)) const expectedContent = delta.create().insert('Hell', { italic: true }, { format: { italic: ['Bob'] } }).insert('o ').insert('World', {}, { delete: ['Bob'] }).insert('attributions', {}, { insert: ['Bob'] }).insert('!') t.assert(attributedContent.equals(expectedContent)) @@ -2187,7 +2187,7 @@ const checkResult = result => { * * @param {t.TestCase} tc */ -export const testAttributionManagerDefaultPerformance = tc => { +export const testRendererDefaultPerformance = tc => { const N = 10000 const MaxDeletionLength = 5 // 25% chance of deletion const MaxInsertionLength = 5 @@ -2212,7 +2212,7 @@ export const testAttributionManagerDefaultPerformance = tc => { ytext.toString() } }) - t.measureTime(`toDelta(attributionManager) performance `, () => { + t.measureTime(`toDelta(renderer) performance `, () => { for (let i = 0; i < M; i++) { ytext.toDelta() } diff --git a/tests/y-xml.tests.js b/tests/y-xml.tests.js index bf120a6a..e2b2dbaf 100644 --- a/tests/y-xml.tests.js +++ b/tests/y-xml.tests.js @@ -139,10 +139,10 @@ export const testFragmentAttributedContent = _tc => { const elem3 = Y.Type.from(delta.create().insert('world')) yfragment.insert(0, [elem1, elem2]) ydoc.get().insert(0, [yfragment]) - let attributionManager = Y.noAttributionsManager + let renderer = Y.baseRenderer ydoc.on('afterTransaction', tr => { - // attributionManager = new TwosetAttributionManager(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertAt', 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, [])) + // renderer = new TwosetRenderer(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertAt', 42), new Y.Attribution('insert', 'kevin')]), createIdMapFromIdSet(tr.deleteSet, [new Y.Attribution('delete', 'kevin')])) + renderer = new Y.TwosetRenderer(Y.createIdMapFromIdSet(tr.insertSet, []), Y.createIdMapFromIdSet(tr.deleteSet, [])) }) t.group('insert / delete', () => { ydoc.transact(() => { @@ -150,10 +150,10 @@ export const testFragmentAttributedContent = _tc => { yfragment.insert(1, [elem3]) }) const expectedContent = delta.create().insert([elem1], null, { delete: [] }).insert([elem2]).insert([elem3], null, { insert: [] }) - const attributedContent = yfragment.toDelta(attributionManager) + const attributedContent = yfragment.toDelta({ renderer }) console.log(attributedContent.toJSON()) t.assert(attributedContent.equals(expectedContent)) - t.compare(elem1.toDelta(attributionManager).toJSON(), delta.create().insert('hello', null, { delete: [] }).toJSON()) + t.compare(elem1.toDelta({ renderer }).toJSON(), delta.create().insert('hello', null, { delete: [] }).toJSON()) }) } @@ -167,10 +167,10 @@ export const testElementAttributedContent = _tc => { const elem2 = delta.create('span').done() const elem3 = delta.create().insert('world').done() yelement.insert(0, [elem1, elem2]) - let attributionManager = Y.noAttributionsManager + let renderer = Y.baseRenderer ydoc.on('afterTransaction', tr => { - // attributionManager = new TwosetAttributionManager(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertAt', 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, [])) + // renderer = new TwosetRenderer(createIdMapFromIdSet(tr.insertSet, [new Y.Attribution('insertAt', 42), new Y.Attribution('insert', 'kevin')]), createIdMapFromIdSet(tr.deleteSet, [new Y.Attribution('delete', 'kevin')])) + renderer = new Y.TwosetRenderer(Y.createIdMapFromIdSet(tr.insertSet, []), Y.createIdMapFromIdSet(tr.deleteSet, [])) }) t.group('insert / delete', () => { ydoc.transact(() => { @@ -183,7 +183,7 @@ export const testElementAttributedContent = _tc => { .insert([elem2]) .insert([delta.create().insert('world', null, { insert: [] })], null, { insert: [] }) .setAttr('key', '42', { insert: [] }) - const attributedContent = yelement.toDeltaDeep(attributionManager) + const attributedContent = yelement.toDeltaDeep({ renderer }) console.log('retrieved content', attributedContent.toJSON()) t.assert(attributedContent.equals(expectedContent)) t.compare(attributedContent.toJSON().attrs, { key: { type: 'insert', value: '42', attribution: { insert: [] } } }) @@ -206,9 +206,9 @@ export const testElementAttributedContentViaDiffer = _tc => { yelement.insert(1, [elem3]) yelement.setAttr('key', '42') }) - const attributionManager = Y.createAttributionManagerFromDiff(ydocV1, ydoc) + const renderer = Y.createDiffRenderer(ydocV1, ydoc) const expectedContent = delta.create().insert([delta.create().insert('hello')], null, { delete: [] }).insert([elem2.toDeltaDeep()]).insert([delta.create().insert('world', null, { insert: [] })], null, { insert: [] }).setAttr('key', '42', { insert: [] }) - const attributedContent = yelement.toDeltaDeep(attributionManager) + const attributedContent = yelement.toDeltaDeep({ renderer }) console.log('children', attributedContent.toJSON().children) console.log('attributes', attributedContent.toJSON().attrs) t.compare(attributedContent.toJSON(), expectedContent.toJSON()) @@ -226,7 +226,7 @@ export const testElementAttributedContentViaDiffer = _tc => { delta.create().insert('world', null, { insert: [] }) ], null, { insert: [] }) .setAttr('key', '42', { insert: [] }) - const attributedContent = yelement.toDeltaDeep(attributionManager) + const attributedContent = yelement.toDeltaDeep({ renderer }) console.log('children', JSON.stringify(attributedContent.toJSON().children, null, 2)) console.log('cs expec', JSON.stringify(expectedContent.toJSON(), null, 2)) console.log('attributes', attributedContent.toJSON().attrs) @@ -238,7 +238,7 @@ export const testElementAttributedContentViaDiffer = _tc => { elem3.insert(0, 'big') }) t.group('test getContentDeep after some more updates', () => { - t.info('expecting diffingAttributionManager to auto update itself') + t.info('expecting DiffRenderer to auto update itself') const expectedContent = delta.create() .insert( [delta.create().insert('hello')], @@ -250,7 +250,7 @@ export const testElementAttributedContentViaDiffer = _tc => { delta.create().insert('bigworld', null, { insert: [] }) ], null, { insert: [] }) .setAttr('key', '42', { insert: [] }) - const attributedContent = yelement.toDeltaDeep(attributionManager) + const attributedContent = yelement.toDeltaDeep({ renderer }) console.log('children', JSON.stringify(attributedContent.toJSON().children, null, 2)) console.log('cs expec', JSON.stringify(expectedContent.toJSON(), null, 2)) console.log('attributes', attributedContent.toJSON().attrs) @@ -260,11 +260,11 @@ export const testElementAttributedContentViaDiffer = _tc => { }) Y.applyUpdate(ydocV1, Y.encodeStateAsUpdate(ydoc)) t.group('test getContentDeep both docs synced', () => { - t.info('expecting diffingAttributionManager to auto update itself') + t.info('expecting DiffRenderer to auto update itself') const expectedContent = delta.create().insert([delta.create('span')]).insert([ delta.create().insert('bigworld') ]).setAttr('key', '42') - const attributedContent = yelement.toDeltaDeep(attributionManager) + const attributedContent = yelement.toDeltaDeep({ renderer }) console.log('children', JSON.stringify(attributedContent.toJSON().children, null, 2)) console.log('cs expec', JSON.stringify(expectedContent.toJSON(), null, 2)) console.log('attributes', attributedContent.toJSON().attrs) @@ -277,7 +277,7 @@ export const testElementAttributedContentViaDiffer = _tc => { /** * @param {t.TestCase} _tc */ -export const testAttributionManagerSimpleExample = _tc => { +export const testRendererSimpleExample = _tc => { const ydoc = new Y.Doc() ydoc.clientID = 0 // create some initial content @@ -295,7 +295,7 @@ export const testAttributionManagerSimpleExample = _tc => { ytext.delete(11, 8) ytext.insert(11, '!') // highlight the changes - console.log(JSON.stringify(ydocFork.get().toDeltaDeep(Y.createAttributionManagerFromDiff(ydoc, ydocFork)), null, 2)) + console.log(JSON.stringify(ydocFork.get().toDeltaDeep({ renderer: Y.createDiffRenderer(ydoc, ydocFork) }), null, 2)) /* => { "children": { @@ -390,7 +390,7 @@ const collectForbiddenOps = (d, forbidden, path = '$', acc = []) => { /** * Reproduces the y-prosemirror issue #247 contract violation at the @y/y - * level: `ytype.toDeltaDeep(am)` is supposed to surface soft-deleted content + * level: `ytype.toDeltaDeep({ renderer })` is supposed to surface soft-deleted content * as positive ops (`SetAttrOp` / `InsertOp`) carrying attribution metadata, * never as `DeleteAttrOp` / `DeleteOp`. Today, when a parent YXmlElement is * itself soft-deleted under attribution, `typeMapGetDelta` (ytype.js:1928) @@ -398,7 +398,7 @@ const collectForbiddenOps = (d, forbidden, path = '$', acc = []) => { * cascaded child setAttr / content items - which downstream consumers * (lib0/delta `diff`, y-prosemirror's PM mapper) cannot handle. * - * Expected after fix: walking the delta returned by `parent.toDeltaDeep(am)` + * Expected after fix: walking the delta returned by `parent.toDeltaDeep({ renderer })` * finds zero `DeleteAttrOp` entries in any `attrs` map and zero `DeleteOp` * entries in any `children` list, at every nesting level. * @@ -424,8 +424,8 @@ export const testToDeltaDeepEmitsNoDeleteOpsForSoftDeletedParent = _tc => { parent.delete(0, 1) }) - const am = Y.createAttributionManagerFromDiff(ydocV1, ydoc) - const rendered = parent.toDeltaDeep(am) + const renderer = Y.createDiffRenderer(ydocV1, ydoc) + const rendered = parent.toDeltaDeep({ renderer }) // The cascade should surface as positive ops with attribution, not as // delete ops. Find any DeleteAttrOp / DeleteOp anywhere in the tree. @@ -439,7 +439,7 @@ export const testToDeltaDeepEmitsNoDeleteOpsForSoftDeletedParent = _tc => { } t.assert( offenders.length === 0, - `toDeltaDeep(am) emitted ${offenders.length} forbidden delete op(s) for a soft-deleted parent (issue #247 / y-prosemirror)` + `toDeltaDeep(renderer) emitted ${offenders.length} forbidden delete op(s) for a soft-deleted parent (issue #247 / y-prosemirror)` ) } @@ -460,8 +460,8 @@ export const testToDeltaDeepRendersExplicitDeleteAttrAsSetAttrWithAttribution = ydoc.transact(() => { ydoc.get('p').deleteAttr('id') }) - const am = Y.createAttributionManagerFromDiff(ydocV1, ydoc) - const rendered = ydoc.get('p').toDeltaDeep(am) + const renderer = Y.createDiffRenderer(ydocV1, ydoc) + const rendered = ydoc.get('p').toDeltaDeep({ renderer }) const offenders = collectForbiddenOps(rendered, ['DeleteAttrOp', 'DeleteOp']) t.assert(offenders.length === 0, 'no DeleteAttrOp / DeleteOp from explicit deleteAttr under diff AM')