mirror of
https://github.com/yjs/yjs.git
synced 2026-08-29 10:09:05 +02:00
rename concept AttributionManager => Renderer
definitely a breaking change to all users of the current attribution api
This commit is contained in:
@@ -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
6
global.d.ts
vendored
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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'
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
74
src/ytype.js
74
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<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)
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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('!'))
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user