rename concept AttributionManager => Renderer

definitely a breaking change to all users of the current attribution api
This commit is contained in:
Kevin Jahns
2026-06-16 21:29:45 +02:00
parent b2fd836017
commit e53a6a0227
16 changed files with 218 additions and 215 deletions

View File

@@ -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.

6
global.d.ts vendored
View File

@@ -46,8 +46,8 @@ declare type Delta<DConf extends DeltaConf> = import('lib0/delta').Delta<DConf>
// @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<T = any> = import('./src/utils/attribution-manager-helpers.js').AttributedContent<T>
declare type AbstractRenderer = import('./src/utils/renderer-helpers.js').AbstractRenderer
declare type Attribution = import('./src/utils/renderer-helpers.js').Attribution
declare type AttributedContent<T = any> = import('./src/utils/renderer-helpers.js').AttributedContent<T>
declare type Snapshot = import('./src/utils/Snapshot.js').Snapshot

View File

@@ -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

View File

@@ -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'

View File

@@ -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<any>} */ (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
}
}

View File

@@ -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<any>} inserts
* @param {IdMap<any>} deletes
@@ -31,7 +31,7 @@ export class TwosetAttributionManager extends ObservableV2 {
this.deletes = deletes
}
get $type () { return $attributionManager }
get $type () { return $renderer }
/**
* @param {Array<AttributedContent<any>>} 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<string>}
@@ -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<ContentAttribute>} [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<AttributedContent<any>>} 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)

View File

@@ -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<DConf> : Delta<import('../ytype.js').DeltaConfDeltaToYType<DConf>>} 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<string|number>} 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)

View File

@@ -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)
}

View File

@@ -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<AttributedContent<any>>} _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<AttributedContent<any>>} 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()

View File

@@ -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<string,any>} 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<AttributedContent<any>>}
*/
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<AttributedContent<any>>}
*/
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<string|null>} */ (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<DConf>}
*/
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<string|null>?} attrsToRender
* @param {AbstractAttributionManager} am
* @param {AbstractRenderer} renderer
* @param {boolean} deep
* @param {Set<YType>|Map<YType,any>|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<AttributedContent>}
*/
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)
}

View File

@@ -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 = () => {
// <p>hi</p>
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())

View File

@@ -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('!'))
}

View File

@@ -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))
})

View File

@@ -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)
})

View File

@@ -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 <executed ${M} times>`, () => {
t.measureTime(`toDelta(renderer) performance <executed ${M} times>`, () => {
for (let i = 0; i < M; i++) {
ytext.toDelta()
}

View File

@@ -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')