[diffing] event returns delta class object, migrate away from legacy deltas, work on snapshots using attribution manager. WIP

This commit is contained in:
Kevin Jahns
2025-05-07 00:35:57 +02:00
parent 2ebb3c98ec
commit d23e3fb167
20 changed files with 687 additions and 687 deletions

8
package-lock.json generated
View File

@@ -9,7 +9,7 @@
"version": "14.0.0-5", "version": "14.0.0-5",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"lib0": "^0.2.105" "lib0": "^0.2.107"
}, },
"devDependencies": { "devDependencies": {
"@types/node": "^22.14.1", "@types/node": "^22.14.1",
@@ -2796,9 +2796,9 @@
} }
}, },
"node_modules/lib0": { "node_modules/lib0": {
"version": "0.2.105", "version": "0.2.107",
"resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.105.tgz", "resolved": "https://registry.npmjs.org/lib0/-/lib0-0.2.107.tgz",
"integrity": "sha512-5vtbuBi2P43ZYOfVMV+TZYkWEa0J9kijXirzEgrPA+nJDQCtMx805/rqW4G1nXbM9IRIhwW+OyNNgcQdbhKfSw==", "integrity": "sha512-2xih/AugT0dJSgeSfsW/bqIPILlsqzEtmw8hXzWEnMLrOz12DTK5z9rjNgUT21/HkBjHSznOQBr67bcZdc8Ltg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"isomorphic.js": "^0.2.4" "isomorphic.js": "^0.2.4"

View File

@@ -24,7 +24,7 @@
"debug": "npm run gentesthtml && 0serve -o test.html", "debug": "npm run gentesthtml && 0serve -o test.html",
"trace-deopt": "clear && node --trace-deopt ./tests/index.js", "trace-deopt": "clear && node --trace-deopt ./tests/index.js",
"trace-opt": "clear && node --trace-opt ./tests/index.js", "trace-opt": "clear && node --trace-opt ./tests/index.js",
"gentesthtml": "0gentesthtml --script ./tests/index.js > test.html" "gentesthtml": "0gentesthtml --script ./tests/index.js --include-dependencies @y/protocols > test.html"
}, },
"exports": { "exports": {
".": { ".": {
@@ -85,7 +85,7 @@
}, },
"homepage": "https://docs.yjs.dev", "homepage": "https://docs.yjs.dev",
"dependencies": { "dependencies": {
"lib0": "^0.2.105" "lib0": "^0.2.107"
}, },
"devDependencies": { "devDependencies": {
"@y/protocols": "^1.0.6-1", "@y/protocols": "^1.0.6-1",

View File

@@ -728,6 +728,8 @@ export const typeListInsertGenericsAfter = (transaction, parent, referenceItem,
case Boolean: case Boolean:
case Array: case Array:
case String: case String:
case BigInt:
case Date:
jsonContent.push(c) jsonContent.push(c)
break break
default: default:
@@ -916,6 +918,8 @@ export const typeMapSet = (transaction, parent, key, value) => {
case Boolean: case Boolean:
case Array: case Array:
case String: case String:
case Date:
case BigInt:
content = new ContentAny([value]) content = new ContentAny([value])
break break
case Uint8Array: case Uint8Array:

View File

@@ -225,8 +225,8 @@ export class YArray extends AbstractType {
*/ */
getContentDeep (am = noAttributionsManager) { getContentDeep (am = noAttributionsManager) {
return this.getContent(am).map(d => /** @type {any} */ ( return this.getContent(am).map(d => /** @type {any} */ (
d instanceof delta.InsertOp && d.insert instanceof Array d instanceof delta.InsertArrayOp && d.insert instanceof Array
? new delta.InsertOp(d.insert.map(e => e instanceof AbstractType ? e.getContentDeep(am) : e), d.attributes, d.attribution) ? new delta.InsertArrayOp(d.insert.map(e => e instanceof AbstractType ? e.getContentDeep(am) : e), d.attributes, d.attribution)
: d : d
)) ))
} }

View File

@@ -7,7 +7,6 @@ import {
AbstractType, AbstractType,
getItemCleanStart, getItemCleanStart,
getState, getState,
isVisible,
createID, createID,
YTextRefID, YTextRefID,
callTypeObservers, callTypeObservers,
@@ -16,7 +15,6 @@ import {
GC, GC,
ContentFormat, ContentFormat,
ContentString, ContentString,
splitSnapshotAffectedStructs,
iterateStructsByIdSet, iterateStructsByIdSet,
findMarker, findMarker,
typeMapDelete, typeMapDelete,
@@ -26,7 +24,7 @@ import {
updateMarkerChanges, updateMarkerChanges,
ContentType, ContentType,
warnPrematureAccess, warnPrematureAccess,
noAttributionsManager, AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, ID, Doc, Item, Snapshot, Transaction, // eslint-disable-line noAttributionsManager, AbstractAttributionManager, ArraySearchMarker, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, Doc, Item, Transaction, // eslint-disable-line
createAttributionFromAttributionItems createAttributionFromAttributionItems
} from '../internals.js' } from '../internals.js'
@@ -622,12 +620,12 @@ export class YTextEvent extends YEvent {
} }
/** /**
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string, delete?:number, retain?:number}>}} * @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:delta.TextDelta}}
*/ */
get changes () { get changes () {
if (this._changes === null) { if (this._changes === null) {
/** /**
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string|AbstractType<any>|object, delete?:number, retain?:number}>}} * @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:delta.TextDelta}}
*/ */
const changes = { const changes = {
keys: this.keys, keys: this.keys,
@@ -644,192 +642,106 @@ export class YTextEvent extends YEvent {
* Compute the changes in the delta format. * Compute the changes in the delta format.
* A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document. * A {@link https://quilljs.com/docs/delta/|Quill Delta}) that represents the changes on the document.
* *
* @type {Array<{insert?:string|object|AbstractType<any>, delete?:number, retain?:number, attributes?: Object<string,any>}>} * @type {delta.TextDelta}
* *
* @public * @public
*/ */
get delta () { get delta () {
if (this._delta === null) { if (this._delta === null) {
const y = /** @type {Doc} */ (this.target.doc) const ydoc = /** @type {Doc} */ (this.target.doc)
/** const d = this._delta = delta.createTextDelta()
* @type {Array<{insert?:string|object|AbstractType<any>, delete?:number, retain?:number, attributes?: Object<string,any>}>} transact(ydoc, transaction => {
*/ /**
const delta = [] * @type {import('../utils/Delta.js').FormattingAttributes}
transact(y, transaction => { */
const currentAttributes = new Map() // saves all current attributes for insert let currentAttributes = {} // saves all current attributes for insert
const oldAttributes = new Map() let usingCurrentAttributes = false
/**
* @type {import('../utils/Delta.js').FormattingAttributes}
*/
let changedAttributes = {} // saves changed attributes for retain
let usingChangedAttributes = false
/**
* @type {import('../utils/Delta.js').FormattingAttributes}
*/
const previousAttributes = {} // The value before changes
const tr = this.transaction
let item = this.target._start let item = this.target._start
/**
* @type {string?}
*/
let action = null
/**
* @type {Object<string,any>}
*/
const attributes = {} // counts added or removed new attributes for retain
/**
* @type {string|object}
*/
let insert = ''
let retain = 0
let deleteLen = 0
const addOp = () => {
if (action !== null) {
/**
* @type {any}
*/
let op = null
switch (action) {
case 'delete':
if (deleteLen > 0) {
op = { delete: deleteLen }
}
deleteLen = 0
break
case 'insert':
if (typeof insert === 'object' || insert.length > 0) {
op = { insert }
if (currentAttributes.size > 0) {
op.attributes = {}
currentAttributes.forEach((value, key) => {
if (value !== null) {
op.attributes[key] = value
}
})
}
}
insert = ''
break
case 'retain':
if (retain > 0) {
op = { retain }
if (!object.isEmpty(attributes)) {
op.attributes = object.assign({}, attributes)
}
}
retain = 0
break
}
if (op) delta.push(op)
action = null
}
}
while (item !== null) { while (item !== null) {
const freshDelete = item.deleted && tr.deleteSet.hasId(item.id) && !tr.insertSet.hasId(item.id)
const freshInsert = !item.deleted && tr.insertSet.hasId(item.id)
switch (item.content.constructor) { switch (item.content.constructor) {
case ContentType: case ContentType:
case ContentEmbed: case ContentEmbed:
if (this.adds(item)) { if (freshInsert) {
if (!this.deletes(item)) { d.usedAttributes = currentAttributes
addOp() usingCurrentAttributes = true
action = 'insert' d.insert(item.content.getContent()[0])
insert = item.content.getContent()[0] } else if (freshDelete) {
addOp() d.delete(1)
}
} else if (this.deletes(item)) {
if (action !== 'delete') {
addOp()
action = 'delete'
}
deleteLen += 1
} else if (!item.deleted) { } else if (!item.deleted) {
if (action !== 'retain') { d.usedAttributes = changedAttributes
addOp() usingChangedAttributes = true
action = 'retain' d.retain(1)
}
retain += 1
} }
break break
case ContentString: case ContentString:
if (this.adds(item)) { if (freshInsert) {
if (!this.deletes(item)) { d.usedAttributes = currentAttributes
if (action !== 'insert') { usingCurrentAttributes = true
addOp() d.insert(/** @type {ContentString} */ (item.content).str)
action = 'insert' } else if (freshDelete) {
} d.delete(item.length)
insert += /** @type {ContentString} */ (item.content).str
}
} else if (this.deletes(item)) {
if (action !== 'delete') {
addOp()
action = 'delete'
}
deleteLen += item.length
} else if (!item.deleted) { } else if (!item.deleted) {
if (action !== 'retain') { d.usedAttributes = changedAttributes
addOp() usingChangedAttributes = true
action = 'retain' d.retain(item.length)
}
retain += item.length
} }
break break
case ContentFormat: { case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (item.content) const { key, value } = /** @type {ContentFormat} */ (item.content)
if (this.adds(item)) { const currAttrVal = currentAttributes[key] ?? null
if (!this.deletes(item)) { if (freshDelete || freshInsert) {
const curVal = currentAttributes.get(key) ?? null // create fresh references
if (!equalAttrs(curVal, value)) { if (usingCurrentAttributes) {
if (action === 'retain') { currentAttributes = object.assign({}, currentAttributes)
addOp() usingCurrentAttributes = false
}
if (equalAttrs(value, (oldAttributes.get(key) ?? null))) {
delete attributes[key]
} else {
attributes[key] = value
}
} else if (value !== null) {
item.delete(transaction)
}
} }
} else if (this.deletes(item)) { if (usingChangedAttributes) {
oldAttributes.set(key, value) usingChangedAttributes = false
const curVal = currentAttributes.get(key) ?? null changedAttributes = object.assign({}, changedAttributes)
if (!equalAttrs(curVal, value)) {
if (action === 'retain') {
addOp()
}
attributes[key] = curVal
}
} else if (!item.deleted) {
oldAttributes.set(key, value)
const attr = attributes[key]
if (attr !== undefined) {
if (!equalAttrs(attr, value)) {
if (action === 'retain') {
addOp()
}
if (value === null) {
delete attributes[key]
} else {
attributes[key] = value
}
} else if (attr !== null) { // this will be cleaned up automatically by the contextless cleanup function
item.delete(transaction)
}
} }
} }
if (!item.deleted) { if (freshInsert) {
if (action === 'insert') { if (equalAttrs(value, currAttrVal)) {
addOp() item.delete(transaction)
} else if (equalAttrs(value, previousAttributes[key] ?? null)) {
delete currentAttributes[key]
delete changedAttributes[key]
} else {
currentAttributes[key] = value
changedAttributes[key] = value
} }
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (item.content)) } else if (freshDelete) {
changedAttributes[key] = currAttrVal
currentAttributes[key] = currAttrVal
previousAttributes[key] = value
} else if (!item.deleted) {
// fresh reference to currentAttributes only
if (usingCurrentAttributes) {
currentAttributes = object.assign({}, currentAttributes)
usingCurrentAttributes = false
}
currentAttributes[key] = value
previousAttributes[key] = value
} }
break break
} }
} }
item = item.right item = item.right
} }
addOp()
while (delta.length > 0) {
const lastOp = delta[delta.length - 1]
if (lastOp.retain !== undefined && lastOp.attributes === undefined) {
// retain delta's if they don't assign attributes
delta.pop()
} else {
break
}
}
}) })
this._delta = delta d.done()
} }
return /** @type {any} */ (this._delta) return /** @type {any} */ (this._delta)
} }
@@ -903,7 +815,7 @@ export class YText extends AbstractType {
*/ */
clone () { clone () {
const text = new YText() const text = new YText()
text.applyDelta(this.toDelta()) text.applyDelta(this.getContent())
return text return text
} }
@@ -957,7 +869,7 @@ export class YText extends AbstractType {
/** /**
* Apply a {@link Delta} on this shared YText type. * Apply a {@link Delta} on this shared YText type.
* *
* @param {Array<any>} delta The changes to apply on this element. * @param {Array<any> | delta.Delta} delta The changes to apply on this element.
* @param {object} opts * @param {object} opts
* @param {boolean} [opts.sanitize] Sanitize input delta. Removes ending newlines if set to true. * @param {boolean} [opts.sanitize] Sanitize input delta. Removes ending newlines if set to true.
* *
@@ -967,16 +879,20 @@ export class YText extends AbstractType {
applyDelta (delta, { sanitize = true } = {}) { applyDelta (delta, { sanitize = true } = {}) {
if (this.doc !== null) { if (this.doc !== null) {
transact(this.doc, transaction => { transact(this.doc, transaction => {
/**
* @type {Array<any>}
*/
const deltaOps = /** @type {Array<any>} */ (/** @type {delta.Delta} */ (delta).ops instanceof Array ? /** @type {delta.Delta} */ (delta).ops : delta)
const currPos = new ItemTextListPosition(null, this._start, 0, new Map()) const currPos = new ItemTextListPosition(null, this._start, 0, new Map())
for (let i = 0; i < delta.length; i++) { for (let i = 0; i < deltaOps.length; i++) {
const op = delta[i] const op = deltaOps[i]
if (op.insert !== undefined) { if (op.insert !== undefined) {
// Quill assumes that the content starts with an empty paragraph. // Quill assumes that the content starts with an empty paragraph.
// Yjs/Y.Text assumes that it starts empty. We always hide that // Yjs/Y.Text assumes that it starts empty. We always hide that
// there is a newline at the end of the content. // there is a newline at the end of the content.
// If we omit this step, clients will see a different number of // If we omit this step, clients will see a different number of
// paragraphs, but nothing bad will happen. // paragraphs, but nothing bad will happen.
const ins = (!sanitize && typeof op.insert === 'string' && i === delta.length - 1 && currPos.right === null && op.insert.slice(-1) === '\n') ? op.insert.slice(0, -1) : op.insert const ins = (!sanitize && typeof op.insert === 'string' && i === deltaOps.length - 1 && currPos.right === null && op.insert.slice(-1) === '\n') ? op.insert.slice(0, -1) : op.insert
if (typeof ins !== 'string' || ins.length > 0) { if (typeof ins !== 'string' || ins.length > 0) {
insertText(transaction, this, currPos, ins, op.attributes || {}) insertText(transaction, this, currPos, ins, op.attributes || {})
} }
@@ -1006,8 +922,8 @@ export class YText extends AbstractType {
*/ */
getContentDeep (am = noAttributionsManager) { getContentDeep (am = noAttributionsManager) {
return this.getContent(am).map(d => return this.getContent(am).map(d =>
d instanceof delta.InsertOp && d.insert instanceof AbstractType d instanceof delta.InsertStringOp && d.insert instanceof AbstractType
? new delta.InsertOp(d.insert.getContent(am), d.attributes, d.attribution) ? new delta.InsertStringOp(d.insert.getContent(am), d.attributes, d.attribution)
: d : d
) )
} }
@@ -1091,121 +1007,6 @@ export class YText extends AbstractType {
return d return d
} }
/**
* Returns the Delta representation of this YText type.
*
* @param {Snapshot} [snapshot]
* @param {Snapshot} [prevSnapshot]
* @param {function('removed' | 'added', ID):any} [computeYChange]
* @return {any} The Delta representation of this type.
*
* @public
*/
toDelta (snapshot, prevSnapshot, computeYChange) {
this.doc ?? warnPrematureAccess()
/**
* @type{Array<any>}
*/
const ops = []
const currentAttributes = new Map()
const doc = /** @type {Doc} */ (this.doc)
let str = ''
let n = this._start
function packStr () {
if (str.length > 0) {
// pack str with attributes to ops
/**
* @type {Object<string,any>}
*/
const attributes = {}
let addAttributes = false
currentAttributes.forEach((value, key) => {
addAttributes = true
attributes[key] = value
})
/**
* @type {Object<string,any>}
*/
const op = { insert: str }
if (addAttributes) {
op.attributes = attributes
}
ops.push(op)
str = ''
}
}
const computeDelta = () => {
while (n !== null) {
if (isVisible(n, snapshot) || (prevSnapshot !== undefined && isVisible(n, prevSnapshot))) {
switch (n.content.constructor) {
case ContentString: {
const cur = currentAttributes.get('ychange')
if (snapshot !== undefined && !isVisible(n, snapshot)) {
if (cur === undefined || cur.user !== n.id.client || cur.type !== 'removed') {
packStr()
currentAttributes.set('ychange', computeYChange ? computeYChange('removed', n.id) : { type: 'removed' })
}
} else if (prevSnapshot !== undefined && !isVisible(n, prevSnapshot)) {
if (cur === undefined || cur.user !== n.id.client || cur.type !== 'added') {
packStr()
currentAttributes.set('ychange', computeYChange ? computeYChange('added', n.id) : { type: 'added' })
}
} else if (cur !== undefined) {
packStr()
currentAttributes.delete('ychange')
}
str += /** @type {ContentString} */ (n.content).str
break
}
case ContentType:
case ContentEmbed: {
packStr()
/**
* @type {Object<string,any>}
*/
const op = {
insert: n.content.getContent()[0]
}
if (currentAttributes.size > 0) {
const attrs = /** @type {Object<string,any>} */ ({})
op.attributes = attrs
currentAttributes.forEach((value, key) => {
attrs[key] = value
})
}
ops.push(op)
break
}
case ContentFormat:
if (isVisible(n, snapshot)) {
packStr()
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (n.content))
}
break
}
}
n = n.right
}
packStr()
}
if (snapshot || prevSnapshot) {
// snapshots are merged again after the transaction, so we need to keep the
// transaction alive until we are done
transact(doc, transaction => {
if (snapshot) {
splitSnapshotAffectedStructs(transaction, snapshot)
}
if (prevSnapshot) {
splitSnapshotAffectedStructs(transaction, prevSnapshot)
}
computeDelta()
}, 'cleanup')
} else {
computeDelta()
}
return ops
}
/** /**
* Insert text at a given index. * Insert text at a given index.
* *

View File

@@ -225,8 +225,8 @@ export class YXmlElement extends YXmlFragment {
getContentDeep (am = noAttributionsManager) { getContentDeep (am = noAttributionsManager) {
const { children: origChildren, attributes: origAttributes } = this.getContent(am) const { children: origChildren, attributes: origAttributes } = this.getContent(am)
const children = origChildren.map(d => /** @type {any} */ ( const children = origChildren.map(d => /** @type {any} */ (
(d instanceof delta.InsertOp && d.insert instanceof Array) (d instanceof delta.InsertArrayOp && d.insert instanceof Array)
? new delta.InsertOp(d.insert.map(e => e instanceof AbstractType ? /** @type {delta.ArrayDelta<Array<any>>} */ (e.getContentDeep(am)) : e), d.attributes, d.attribution) ? new delta.InsertArrayOp(d.insert.map(e => e instanceof AbstractType ? /** @type {delta.ArrayDelta<Array<any>>} */ (e.getContentDeep(am)) : e), d.attributes, d.attribution)
: d : d
)) ))
/** /**

View File

@@ -403,8 +403,8 @@ export class YXmlFragment extends AbstractType {
* @type {import('../utils/Delta.js').ArrayDelta<Array<import('./AbstractType.js').YXmlDeepContent>>} * @type {import('../utils/Delta.js').ArrayDelta<Array<import('./AbstractType.js').YXmlDeepContent>>}
*/ */
const children = origChildren.map(d => /** @type {any} */ ( const children = origChildren.map(d => /** @type {any} */ (
d instanceof delta.InsertOp && d.insert instanceof Array d instanceof delta.InsertArrayOp && d.insert instanceof Array
? new delta.InsertOp(d.insert.map(e => e instanceof AbstractType ? e.getContentDeep(am) : e), d.attributes, d.attribution) ? new delta.InsertArrayOp(d.insert.map(e => e instanceof AbstractType ? e.getContentDeep(am) : e), d.attributes, d.attribution)
: d : d
)) ))
return { children } return { children }

View File

@@ -4,6 +4,8 @@ import {
ContentType, YXmlElement, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, // eslint-disable-line ContentType, YXmlElement, UpdateDecoderV1, UpdateDecoderV2, UpdateEncoderV1, UpdateEncoderV2, // eslint-disable-line
} from '../internals.js' } from '../internals.js'
import * as delta from '../utils/Delta.js'
/** /**
* Represents text in a Dom Element. In the future this type will also handle * Represents text in a Dom Element. In the future this type will also handle
* simple formatting information like bold and italic. * simple formatting information like bold and italic.
@@ -38,7 +40,7 @@ export class YXmlText extends YText {
*/ */
clone () { clone () {
const text = new YXmlText() const text = new YXmlText()
text.applyDelta(this.toDelta()) text.applyDelta(this.getContent())
return text return text
} }
@@ -66,36 +68,38 @@ export class YXmlText extends YText {
} }
toString () { toString () {
// @ts-ignore return this.getContent().ops.map(dop => {
return this.toDelta().map(delta => { if (dop instanceof delta.InsertStringOp) {
const nestedNodes = [] const nestedNodes = []
for (const nodeName in delta.attributes) { for (const nodeName in dop.attributes) {
const attrs = [] const attrs = []
for (const key in delta.attributes[nodeName]) { for (const key in dop.attributes[nodeName]) {
attrs.push({ key, value: delta.attributes[nodeName][key] }) attrs.push({ key, value: dop.attributes[nodeName][key] })
}
// sort attributes to get a unique order
attrs.sort((a, b) => a.key < b.key ? -1 : 1)
nestedNodes.push({ nodeName, attrs })
} }
// sort attributes to get a unique order // sort node order to get a unique order
attrs.sort((a, b) => a.key < b.key ? -1 : 1) nestedNodes.sort((a, b) => a.nodeName < b.nodeName ? -1 : 1)
nestedNodes.push({ nodeName, attrs }) // now convert to dom string
} let str = ''
// sort node order to get a unique order for (let i = 0; i < nestedNodes.length; i++) {
nestedNodes.sort((a, b) => a.nodeName < b.nodeName ? -1 : 1) const node = nestedNodes[i]
// now convert to dom string str += `<${node.nodeName}`
let str = '' for (let j = 0; j < node.attrs.length; j++) {
for (let i = 0; i < nestedNodes.length; i++) { const attr = node.attrs[j]
const node = nestedNodes[i] str += ` ${attr.key}="${attr.value}"`
str += `<${node.nodeName}` }
for (let j = 0; j < node.attrs.length; j++) { str += '>'
const attr = node.attrs[j]
str += ` ${attr.key}="${attr.value}"`
} }
str += '>' str += dop.insert
for (let i = nestedNodes.length - 1; i >= 0; i--) {
str += `</${nestedNodes[i].nodeName}>`
}
return str
} }
str += delta.insert return ''
for (let i = nestedNodes.length - 1; i >= 0; i--) {
str += `</${nestedNodes[i].nodeName}>`
}
return str
}).join('') }).join('')
} }

View File

@@ -5,10 +5,14 @@ import {
createDeleteSetFromStructStore, createDeleteSetFromStructStore,
createIdMapFromIdSet, createIdMapFromIdSet,
ContentDeleted, ContentDeleted,
Doc, Item, AbstractContent, IdMap, // eslint-disable-line Snapshot, Doc, Item, AbstractContent, IdMap, // eslint-disable-line
insertIntoIdMap, insertIntoIdMap,
insertIntoIdSet, insertIntoIdSet,
diffIdMap diffIdMap,
createIdMap,
createAttributionItem,
mergeIdMaps,
AttributionItem
} from '../internals.js' } from '../internals.js'
import * as error from 'lib0/error' import * as error from 'lib0/error'
@@ -90,6 +94,8 @@ export class AbstractAttributionManager {
readContent (_contents, _item) { readContent (_contents, _item) {
error.methodUnimplemented() error.methodUnimplemented()
} }
destroy () {}
} }
/** /**
@@ -105,6 +111,8 @@ export class TwosetAttributionManager {
this.deletes = deletes this.deletes = deletes
} }
destroy () {}
/** /**
* @param {Array<AttributedContent<any>>} contents * @param {Array<AttributedContent<any>>} contents
* @param {Item} item * @param {Item} item
@@ -131,6 +139,8 @@ export class TwosetAttributionManager {
* @implements AbstractAttributionManager * @implements AbstractAttributionManager
*/ */
export class NoAttributionsManager { export class NoAttributionsManager {
destroy () {}
/** /**
* @param {Array<AttributedContent<any>>} contents * @param {Array<AttributedContent<any>>} contents
* @param {Item} item * @param {Item} item
@@ -243,3 +253,59 @@ export class DiffAttributionManager {
* @param {Doc} nextDoc * @param {Doc} nextDoc
*/ */
export const createAttributionManagerFromDiff = (prevDoc, nextDoc) => new DiffAttributionManager(prevDoc, nextDoc) export const createAttributionManagerFromDiff = (prevDoc, nextDoc) => new DiffAttributionManager(prevDoc, nextDoc)
/**
* Intended for projects that used the v13 snapshot feature. With this AttributionManager you can
* read content similar to the previous snapshot api. Requires that `ydoc.gc` is turned off.
*
* @implements AbstractAttributionManager
*/
export class SnapshotAttributionManager {
/**
* @param {Snapshot} prevSnapshot
* @param {Snapshot} nextSnapshot
*/
constructor (prevSnapshot, nextSnapshot) {
this.prevSnapshot = prevSnapshot
this.nextSnapshot = nextSnapshot
const inserts = createIdMap()
const deletes = createIdMapFromIdSet(diffIdSet(nextSnapshot.ds, prevSnapshot.ds), [createAttributionItem('change', '')])
nextSnapshot.sv.forEach((clock, client) => {
inserts.add(client, 0, prevSnapshot.sv.get(client) || 0, [])
inserts.add(client, prevSnapshot.sv.get(client) || 0, clock, [createAttributionItem('change', '')])
})
this.attrs = mergeIdMaps([diffIdMap(inserts, prevSnapshot.ds), deletes])
}
destroy () { }
/**
* @param {Array<AttributedContent<any>>} contents
* @param {Item} item
*/
readContent (contents, item) {
if ((this.nextSnapshot.sv.get(item.id.client) ?? 0) <= item.id.clock) return // future item that should not be displayed
const slice = this.attrs.slice(item.id, item.length)
let content = slice.length === 1 ? item.content : item.content.copy()
slice.forEach(s => {
const deleted = this.nextSnapshot.ds.has(item.id.client, s.clock)
const c = content
if (s.len < c.getLength()) {
content = c.splice(s.len)
}
if (!deleted || (s.attrs != null && s.attrs.length > 0)) {
let attrsWithoutChange = s.attrs?.filter(attr => attr.name !== 'change') ?? null
if (s.attrs?.length === 0) {
attrsWithoutChange = null
}
contents.push(new AttributedContent(c, deleted, attrsWithoutChange))
}
})
}
}
/**
* @param {Snapshot} prevSnapshot
* @param {Snapshot} nextSnapshot
*/
export const createAttributionManagerFromSnapshots = (prevSnapshot, nextSnapshot = prevSnapshot) => new SnapshotAttributionManager(prevSnapshot, nextSnapshot)

View File

@@ -1,10 +1,22 @@
import * as object from 'lib0/object' import * as object from 'lib0/object'
import * as fun from 'lib0/function' import * as fun from 'lib0/function'
import * as traits from 'lib0/traits' import * as traits from 'lib0/traits'
import * as error from 'lib0/error'
/** /**
* @template {string|Array<any>|{[key: string]: any}} Content * @template {any} ArrayContent
* @typedef {InsertOp<Content>|RetainOp|DeleteOp} DeltaOp * @template {{[key: string]: any}} Embeds
* @typedef {InsertStringOp|InsertEmbedOp<Embeds>|InsertArrayOp<ArrayContent>|RetainOp|DeleteOp} DeltaOp
*/
/**
* @template {{[key: string]: any}} Embeds
* @typedef {InsertStringOp|InsertEmbedOp<Embeds>|RetainOp|DeleteOp} TextDeltaOp
*/
/**
* @template {any} ArrayContent
* @typedef {InsertArrayOp<ArrayContent>|RetainOp|DeleteOp} ArrayDeltaOp
*/ */
/** /**
@@ -16,11 +28,16 @@ import * as traits from 'lib0/traits'
*/ */
/** /**
* @template {string|Array<any>|{[key: string]: any}} Content * @typedef {Array<DeltaJsonOp>} DeltaJson
*/ */
export class InsertOp {
/**
* @typedef {{ insert: string|object, attributes?: { [key: string]: any }, attribution?: Attribution } | { delete: number } | { retain: number, attributes?: { [key:string]: any }, attribution?: Attribution }} DeltaJsonOp
*/
export class InsertStringOp {
/** /**
* @param {Content} insert * @param {string} insert
* @param {FormattingAttributes|null} attributes * @param {FormattingAttributes|null} attributes
* @param {Attribution|null} attribution * @param {Attribution|null} attribution
*/ */
@@ -34,12 +51,83 @@ export class InsertOp {
return (this.insert.constructor === Array || this.insert.constructor === String) ? this.insert.length : 1 return (this.insert.constructor === Array || this.insert.constructor === String) ? this.insert.length : 1
} }
/**
* @return {DeltaJsonOp}
*/
toJSON () { toJSON () {
return object.assign({ insert: this.insert }, this.attributes ? { attributes: this.attributes } : ({}), this.attribution ? { attribution: this.attribution } : ({})) return object.assign({ insert: this.insert }, this.attributes ? { attributes: this.attributes } : ({}), this.attribution ? { attribution: this.attribution } : ({}))
} }
/** /**
* @param {InsertOp<Content>} other * @param {InsertStringOp} other
*/
[traits.EqualityTraitSymbol] (other) {
return fun.equalityDeep(this.insert, other.insert) && fun.equalityDeep(this.attributes, other.attributes) && fun.equalityDeep(this.attribution, other.attribution)
}
}
/**
* @template {any} ArrayContent
*/
export class InsertArrayOp {
/**
* @param {Array<ArrayContent>} insert
* @param {FormattingAttributes|null} attributes
* @param {Attribution|null} attribution
*/
constructor (insert, attributes, attribution) {
this.insert = insert
this.attributes = attributes
this.attribution = attribution
}
get length () {
return this.insert.length
}
/**
* @return {DeltaJsonOp}
*/
toJSON () {
return object.assign({ insert: this.insert }, this.attributes ? { attributes: this.attributes } : ({}), this.attribution ? { attribution: this.attribution } : ({}))
}
/**
* @param {InsertArrayOp<ArrayContent>} other
*/
[traits.EqualityTraitSymbol] (other) {
return fun.equalityDeep(this.insert, other.insert) && fun.equalityDeep(this.attributes, other.attributes) && fun.equalityDeep(this.attribution, other.attribution)
}
}
/**
* @template {{[key: string]: any}} Embeds
*/
export class InsertEmbedOp {
/**
* @param {Embeds} insert
* @param {FormattingAttributes|null} attributes
* @param {Attribution|null} attribution
*/
constructor (insert, attributes, attribution) {
this.insert = insert
this.attributes = attributes
this.attribution = attribution
}
get length () {
return 1
}
/**
* @return {DeltaJsonOp}
*/
toJSON () {
return object.assign({ insert: this.insert }, this.attributes ? { attributes: this.attributes } : ({}), this.attribution ? { attribution: this.attribution } : ({}))
}
/**
* @param {InsertEmbedOp<Embeds>} other
*/ */
[traits.EqualityTraitSymbol] (other) { [traits.EqualityTraitSymbol] (other) {
return fun.equalityDeep(this.insert, other.insert) && fun.equalityDeep(this.attributes, other.attributes) && fun.equalityDeep(this.attribution, other.attribution) return fun.equalityDeep(this.insert, other.insert) && fun.equalityDeep(this.attributes, other.attributes) && fun.equalityDeep(this.attribution, other.attribution)
@@ -58,6 +146,9 @@ export class DeleteOp {
return 0 return 0
} }
/**
* @return {DeltaJsonOp}
*/
toJSON () { toJSON () {
return { delete: this.delete } return { delete: this.delete }
} }
@@ -86,6 +177,9 @@ export class RetainOp {
return this.retain return this.retain
} }
/**
* @return {DeltaJsonOp}
*/
toJSON () { toJSON () {
return object.assign({ retain: this.retain }, this.attributes ? { attributes: this.attributes } : {}, this.attribution ? { attribution: this.attribution } : {}) return object.assign({ retain: this.retain }, this.attributes ? { attributes: this.attributes } : {}, this.attribution ? { attribution: this.attribution } : {})
} }
@@ -98,25 +192,17 @@ export class RetainOp {
} }
} }
/**
* @typedef {Array<any>} ArrayDeltaContent
*/
/** /**
* @typedef {string | { [key: string]: any }} TextDeltaContent * @typedef {string | { [key: string]: any }} TextDeltaContent
*/ */
/** /**
* @typedef {{ array: ArrayDeltaContent, text: TextDeltaContent, custom: string|Array<any>|{[key:string]:any}}} DeltaTypeMapper * @typedef {(TextDelta<any> | ArrayDelta<any>)} Delta
*/
/**
* @typedef {(TextDelta | ArrayDelta)} Delta
*/ */
/** /**
* @template {'array' | 'text' | 'custom'} Type * @template {'array' | 'text' | 'custom'} Type
* @template {DeltaTypeMapper[Type]} [Content=DeltaTypeMapper[Type]] * @template {DeltaOp<any,any>} TDeltaOp
*/ */
export class AbstractDelta { export class AbstractDelta {
/** /**
@@ -125,26 +211,26 @@ export class AbstractDelta {
constructor (type) { constructor (type) {
this.type = type this.type = type
/** /**
* @type {Array<DeltaOp<Content>>} * @type {Array<TDeltaOp>}
*/ */
this.ops = [] this.ops = []
} }
/** /**
* @template {DeltaTypeMapper[Type]} MContent * @template {(d:TDeltaOp) => DeltaOp<any,any>} Mapper
* @param {(d:DeltaOp<Content>)=>DeltaOp<MContent>} f * @param {Mapper} f
* @return {DeltaBuilder<Type, MContent>} * @return {DeltaBuilder<Type, Mapper extends (d:TDeltaOp) => infer OP ? OP : unknown>}
*/ */
map (f) { map (f) {
const d = /** @type {DeltaBuilder<Type,any>} */ (new /** @type {any} */ (this.constructor)(this.type)) const d = /** @type {DeltaBuilder<Type,any>} */ (new /** @type {any} */ (this.constructor)(this.type))
d.ops = this.ops.map(f) d.ops = this.ops.map(f)
// @ts-ignore // @ts-ignore
d._lastOp = d.ops[d.ops.length - 1] ?? null d.lastOp = d.ops[d.ops.length - 1] ?? null
return d return d
} }
/** /**
* @param {(d:DeltaOp<Content>,index:number)=>void} f * @param {(d:TDeltaOp,index:number)=>void} f
*/ */
forEach (f) { forEach (f) {
for ( for (
@@ -157,22 +243,25 @@ export class AbstractDelta {
} }
/** /**
* @param {AbstractDelta<Type, Content>} other * @param {AbstractDelta<Type, TDeltaOp>} other
* @return {boolean} * @return {boolean}
*/ */
equals (other) { equals (other) {
return this[traits.EqualityTraitSymbol](other) return this[traits.EqualityTraitSymbol](other)
} }
/**
* @returns {DeltaJson}
*/
toJSON () { toJSON () {
return { ops: this.ops.map(o => o.toJSON()) } return this.ops.map(o => o.toJSON())
} }
/** /**
* @param {AbstractDelta<Type,Content>} other * @param {AbstractDelta<Type,TDeltaOp>} other
*/ */
[traits.EqualityTraitSymbol] (other) { [traits.EqualityTraitSymbol] (other) {
return this.type === other.type && fun.equalityDeep(this.ops, other.ops) return fun.equalityDeep(this.ops, other.ops)
} }
} }
@@ -184,15 +273,14 @@ export class AbstractDelta {
* @param {T | null} b * @param {T | null} b
*/ */
const mergeAttrs = (a, b) => { const mergeAttrs = (a, b) => {
const merged = a == null ? b : (b == null ? a : object.assign({}, a, b)) const merged = object.isEmpty(a) ? b : (object.isEmpty(b) ? a : object.assign({}, a, b))
if (merged == null || object.isEmpty(merged)) { return null } return object.isEmpty(merged) ? null : merged
return merged
} }
/** /**
* @template {'array' | 'text' | 'custom'} [Type='custom'] * @template {'array' | 'text' | 'custom'} [Type='custom']
* @template {DeltaTypeMapper[Type]} [Content=DeltaTypeMapper[Type]] * @template {DeltaOp<any,any>} [TDeltaOp=DeltaOp<any,any>]
* @extends AbstractDelta<Type,Content> * @extends AbstractDelta<Type,TDeltaOp>
*/ */
export class DeltaBuilder extends AbstractDelta { export class DeltaBuilder extends AbstractDelta {
/** /**
@@ -209,10 +297,9 @@ export class DeltaBuilder extends AbstractDelta {
*/ */
this.usedAttribution = null this.usedAttribution = null
/** /**
* @private * @type {TDeltaOp?}
* @type {DeltaOp<Content>?}
*/ */
this._lastOp = null this.lastOp = null
} }
/** /**
@@ -220,8 +307,44 @@ export class DeltaBuilder extends AbstractDelta {
* @return {this} * @return {this}
*/ */
useAttributes (attributes) { useAttributes (attributes) {
if (this.usedAttributes === attributes) return this this.usedAttributes = object.isEmpty(attributes) ? null : object.assign({}, attributes)
this.usedAttributes = attributes && (object.isEmpty(attributes) ? null : object.assign({}, attributes)) return this
}
/**
* @param {string} name
* @param {any} value
*/
updateUsedAttributes (name, value) {
if (value == null) {
this.usedAttributes = object.assign({}, this.usedAttributes)
delete this.usedAttributes?.[name]
if (object.isEmpty(this.usedAttributes)) {
this.usedAttributes = null
}
} else if (!fun.equalityDeep(this.usedAttributes?.[name], value)) {
this.usedAttributes = object.assign({}, this.usedAttributes)
this.usedAttributes[name] = value
}
return this
}
/**
* @template {keyof Attribution} NAME
* @param {NAME} name
* @param {Attribution[NAME]?} value
*/
updateUsedAttribution (name, value) {
if (value == null) {
this.usedAttribution = object.assign({}, this.usedAttribution)
delete this.usedAttribution?.[name]
if (object.isEmpty(this.usedAttribution)) {
this.usedAttribution = null
}
} else if (!fun.equalityDeep(this.usedAttribution?.[name], value)) {
this.usedAttribution = object.assign({}, this.usedAttribution)
this.usedAttribution[name] = value
}
return this return this
} }
@@ -229,32 +352,30 @@ export class DeltaBuilder extends AbstractDelta {
* @param {Attribution?} attribution * @param {Attribution?} attribution
*/ */
useAttribution (attribution) { useAttribution (attribution) {
if (this.usedAttribution === attribution) return this this.usedAttribution = object.isEmpty(attribution) ? null : object.assign({}, attribution)
this.usedAttribution = attribution && (object.isEmpty(attribution) ? null : object.assign({}, attribution))
return this return this
} }
/** /**
* @param {Content} insert * @param {(TDeltaOp extends TextDelta<infer Embeds> ? string | Embeds : never) | (TDeltaOp extends InsertArrayOp<infer Content> ? Array<Content> : never) } insert
* @param {FormattingAttributes?} attributes * @param {FormattingAttributes?} attributes
* @param {Attribution?} attribution * @param {Attribution?} attribution
* @return {this} * @return {this}
*/ */
insert (insert, attributes = null, attribution = null) { insert (insert, attributes = null, attribution = null) {
const mergedAttributes = attributes == null ? this.usedAttributes : mergeAttrs(this.usedAttributes, attributes) const mergedAttributes = mergeAttrs(this.usedAttributes, attributes)
const mergedAttribution = attribution == null ? this.usedAttribution : mergeAttrs(this.usedAttribution, attribution) const mergedAttribution = mergeAttrs(this.usedAttribution, attribution)
if (this._lastOp instanceof InsertOp && (mergedAttributes === this._lastOp.attributes || fun.equalityDeep(mergedAttributes, this._lastOp.attributes)) && (mergedAttribution === this._lastOp.attribution || fun.equalityDeep(mergedAttribution, this._lastOp.attribution))) { if (((this.lastOp instanceof InsertStringOp && insert.constructor === String) || (this.lastOp instanceof InsertArrayOp && insert.constructor === Array)) && (mergedAttributes === this.lastOp.attributes || fun.equalityDeep(mergedAttributes, this.lastOp.attributes)) && (mergedAttribution === this.lastOp.attribution || fun.equalityDeep(mergedAttribution, this.lastOp.attribution))) {
if (insert.constructor === String) { if (insert.constructor === String) {
// @ts-ignore // @ts-ignore
this._lastOp.insert += insert this.lastOp.insert += insert
} else if (insert.constructor === Array && this._lastOp.insert.constructor === Array) {
// @ts-ignore
this._lastOp.insert.push(...insert)
} else { } else {
this.ops.push(this._lastOp = new InsertOp(insert, mergedAttributes, mergedAttribution)) // @ts-ignore
this.lastOp.insert.push(...insert)
} }
} else { } else {
this.ops.push(this._lastOp = new InsertOp(insert, mergedAttributes, mergedAttribution)) const OpConstructor = /** @type {any} */ (insert.constructor === String ? InsertStringOp : (insert.constructor === Array ? InsertArrayOp : InsertEmbedOp))
this.ops.push(this.lastOp = new OpConstructor(insert, object.isEmpty(mergedAttributes) ? null : mergedAttributes, object.isEmpty(mergedAttribution) ? null : mergedAttribution))
} }
return this return this
} }
@@ -268,10 +389,11 @@ export class DeltaBuilder extends AbstractDelta {
retain (retain, attributes = null, attribution = null) { retain (retain, attributes = null, attribution = null) {
const mergedAttributes = mergeAttrs(this.usedAttributes, attributes) const mergedAttributes = mergeAttrs(this.usedAttributes, attributes)
const mergedAttribution = mergeAttrs(this.usedAttribution, attribution) const mergedAttribution = mergeAttrs(this.usedAttribution, attribution)
if (this._lastOp instanceof RetainOp && fun.equalityDeep(mergedAttributes, this._lastOp.attributes) && fun.equalityDeep(mergedAttribution, this._lastOp.attribution)) { if (this.lastOp instanceof RetainOp && fun.equalityDeep(mergedAttributes, this.lastOp.attributes) && fun.equalityDeep(mergedAttribution, this.lastOp.attribution)) {
this._lastOp.retain += retain this.lastOp.retain += retain
} else { } else {
this.ops.push(this._lastOp = new RetainOp(retain, mergedAttributes, mergedAttribution)) // @ts-ignore
this.ops.push(this.lastOp = new RetainOp(retain, mergedAttributes, mergedAttribution))
} }
return this return this
} }
@@ -281,25 +403,30 @@ export class DeltaBuilder extends AbstractDelta {
* @return {this} * @return {this}
*/ */
delete (len) { delete (len) {
if (this._lastOp instanceof DeleteOp) { if (this.lastOp instanceof DeleteOp) {
this._lastOp.delete += len this.lastOp.delete += len
} else { } else {
this.ops.push(this._lastOp = new DeleteOp(len)) // @ts-ignore
this.ops.push(this.lastOp = new DeleteOp(len))
} }
return this return this
} }
/** /**
* @return {AbstractDelta<Type,Content>} * @return {AbstractDelta<Type,TDeltaOp>}
*/ */
done () { done () {
while (this.lastOp != null && this.lastOp instanceof RetainOp && this.lastOp.attributes === null) {
this.ops.pop()
this.lastOp = this.ops[this.ops.length - 1] ?? null
}
return this return this
} }
} }
/** /**
* @template {ArrayDeltaContent} [Content=ArrayDeltaContent] * @template {any} ArrayContent
* @extends DeltaBuilder<'array',Content> * @extends DeltaBuilder<'array', ArrayDeltaOp<ArrayContent>>>
*/ */
export class ArrayDelta extends DeltaBuilder { export class ArrayDelta extends DeltaBuilder {
constructor () { constructor () {
@@ -308,8 +435,8 @@ export class ArrayDelta extends DeltaBuilder {
} }
/** /**
* @template {TextDeltaContent} [Content=TextDeltaContent] * @template {{ [key:string]: any }} Embeds
* @extends DeltaBuilder<'text',Content> * @extends DeltaBuilder<'text',TextDeltaOp<Embeds>>
*/ */
export class TextDelta extends DeltaBuilder { export class TextDelta extends DeltaBuilder {
constructor () { constructor () {
@@ -323,6 +450,28 @@ export class TextDelta extends DeltaBuilder {
export const createTextDelta = () => new TextDelta() export const createTextDelta = () => new TextDelta()
/** /**
* @return {ArrayDelta<ArrayDeltaContent>} * @return {ArrayDelta<any>}
*/ */
export const createArrayDelta = () => new ArrayDelta() export const createArrayDelta = () => new ArrayDelta()
/**
* @param {DeltaJson} ops
* @param {'custom' | 'text' | 'array'} type
*/
export const fromJSON = (ops, type = 'custom') => {
const d = new DeltaBuilder(type)
for (let i = 0; i < ops.length; i++) {
const op = /** @type {any} */ (ops[i])
// @ts-ignore
if (op.insert !== undefined) {
d.insert(op.insert, op.attributes, op.attribution)
} else if (op.retain !== undefined) {
d.retain(op.retain, op.attributes ?? null, op.attribution ?? null)
} else if (op.delete !== undefined) {
d.delete(op.delete)
} else {
error.unexpectedCase()
}
}
return d.done()
}

View File

@@ -6,6 +6,14 @@ import * as set from 'lib0/set'
import * as array from 'lib0/array' import * as array from 'lib0/array'
import * as error from 'lib0/error' import * as error from 'lib0/error'
/**
* @typedef {import('../utils/Delta.js').TextDelta} TextDelta
*/
/**
* @typedef {import('../utils/Delta.js').Delta} Delta
*/
const errorComputeChanges = 'You must not compute changes after the event-handler fired.' const errorComputeChanges = 'You must not compute changes after the event-handler fired.'
/** /**
@@ -42,7 +50,7 @@ export class YEvent {
*/ */
this._keys = null this._keys = null
/** /**
* @type {null | Array<{ insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any> }>} * @type {TextDelta?}
*/ */
this._delta = null this._delta = null
/** /**
@@ -142,7 +150,7 @@ export class YEvent {
* unexpected behavior (incorrect computation of deltas). A safe way to collect changes * unexpected behavior (incorrect computation of deltas). A safe way to collect changes
* is to store the `changes` or the `delta` object. Avoid storing the `transaction` object. * is to store the `changes` or the `delta` object. Avoid storing the `transaction` object.
* *
* @type {Array<{insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any>}>} * @type {Delta}
*/ */
get delta () { get delta () {
return this.changes.delta return this.changes.delta
@@ -166,7 +174,7 @@ export class YEvent {
* unexpected behavior (incorrect computation of deltas). A safe way to collect changes * unexpected behavior (incorrect computation of deltas). A safe way to collect changes
* is to store the `changes` or the `delta` object. Avoid storing the `transaction` object. * is to store the `changes` or the `delta` object. Avoid storing the `transaction` object.
* *
* @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Array<{insert?:Array<any>|string, delete?:number, retain?:number}>}} * @type {{added:Set<Item>,deleted:Set<Item>,keys:Map<string,{action:'add'|'update'|'delete',oldValue:any}>,delta:Delta}}
*/ */
get changes () { get changes () {
let changes = this._changes let changes = this._changes

View File

@@ -157,23 +157,23 @@
"lib0/performance.js": "./node_modules/lib0/performance.js", "lib0/performance.js": "./node_modules/lib0/performance.js",
"lib0/dist/performance.cjs": "./node_modules/lib0/dist/performance.node.cjs", "lib0/dist/performance.cjs": "./node_modules/lib0/dist/performance.node.cjs",
"lib0/performance": "./node_modules/lib0/performance.js", "lib0/performance": "./node_modules/lib0/performance.js",
"y-protocols/package.json": "./node_modules/y-protocols/package.json", "@y/protocols/package.json": "./node_modules/@y/protocols/package.json",
"y-protocols/sync.js": "./node_modules/y-protocols/sync.js", "@y/protocols/sync.js": "./node_modules/@y/protocols/sync.js",
"y-protocols/dist/sync.cjs": "./node_modules/y-protocols/dist/sync.cjs", "@y/protocols/dist/sync.cjs": "./node_modules/@y/protocols/dist/sync.cjs",
"y-protocols/sync": "./node_modules/y-protocols/sync.js", "@y/protocols/sync": "./node_modules/@y/protocols/sync.js",
"y-protocols/awareness.js": "./node_modules/y-protocols/awareness.js", "@y/protocols/awareness.js": "./node_modules/@y/protocols/awareness.js",
"y-protocols/dist/awareness.cjs": "./node_modules/y-protocols/dist/awareness.cjs", "@y/protocols/dist/awareness.cjs": "./node_modules/@y/protocols/dist/awareness.cjs",
"y-protocols/awareness": "./node_modules/y-protocols/awareness.js", "@y/protocols/awareness": "./node_modules/@y/protocols/awareness.js",
"y-protocols/auth.js": "./node_modules/y-protocols/auth.js", "@y/protocols/auth.js": "./node_modules/@y/protocols/auth.js",
"y-protocols/dist/auth.cjs": "./node_modules/y-protocols/dist/auth.cjs", "@y/protocols/dist/auth.cjs": "./node_modules/@y/protocols/dist/auth.cjs",
"y-protocols/auth": "./node_modules/y-protocols/auth.js" "@y/protocols/auth": "./node_modules/@y/protocols/auth.js"
}, },
"scopes": { "scopes": {
"./node_modules/lib0/": { "./node_modules/lib0/": {
"isomorphic.js": "./node_modules/isomorphic.js/browser.mjs", "isomorphic.js": "./node_modules/isomorphic.js/browser.mjs",
"isomorphic.js/package.json": "./node_modules/isomorphic.js/package.json" "isomorphic.js/package.json": "./node_modules/isomorphic.js/package.json"
}, },
"./node_modules/y-protocols/": { "./node_modules/@y/protocols/": {
"lib0/package.json": "./node_modules/lib0/package.json", "lib0/package.json": "./node_modules/lib0/package.json",
"lib0": "./node_modules/lib0/index.js", "lib0": "./node_modules/lib0/index.js",
"lib0/array.js": "./node_modules/lib0/array.js", "lib0/array.js": "./node_modules/lib0/array.js",

File diff suppressed because one or more lines are too long

View File

@@ -6,7 +6,7 @@ import * as delta from '../src/utils/Delta.js'
*/ */
export const testDelta = _tc => { export const testDelta = _tc => {
const d = delta.createTextDelta().insert('hello').insert(' ').useAttributes({ bold: true }).insert('world').useAttribution({ insert: ['tester'] }).insert('!').done() const d = delta.createTextDelta().insert('hello').insert(' ').useAttributes({ bold: true }).insert('world').useAttribution({ insert: ['tester'] }).insert('!').done()
t.compare(d.toJSON().ops, [{ insert: 'hello ' }, { insert: 'world', attributes: { bold: true } }, { insert: '!', attributes: { bold: true }, attribution: { insert: ['tester'] } }]) t.compare(d.toJSON(), [{ insert: 'hello ' }, { insert: 'world', attributes: { bold: true } }, { insert: '!', attributes: { bold: true }, attribution: { insert: ['tester'] } }])
} }
/** /**
@@ -21,5 +21,59 @@ export const testDeltaMerging = _tc => {
.insert([1]) .insert([1])
.insert([2]) .insert([2])
.done() .done()
t.compare(d.toJSON().ops, [{ insert: 'helloworld' }, { insert: ' ', attributes: { italic: true } }, { insert: {} }, { insert: [1, 2] }]) t.compare(d.toJSON(), [{ insert: 'helloworld' }, { insert: ' ', attributes: { italic: true } }, { insert: {} }, { insert: [1, 2] }])
}
/**
* @param {t.TestCase} _tc
*/
export const testUseAttributes = _tc => {
const d = delta.createTextDelta()
.insert('a')
.updateUsedAttributes('bold', true)
.insert('b')
.insert('c', { bold: 4 })
.updateUsedAttributes('bold', null)
.insert('d')
.useAttributes({ italic: true })
.insert('e')
.useAttributes(null)
.insert('f')
.done()
const d2 = delta.createTextDelta()
.insert('a')
.insert('b', { bold: true })
.insert('c', { bold: 4 })
.insert('d')
.insert('e', { italic: true })
.insert('f')
.done()
t.compare(d, d2)
}
/**
* @param {t.TestCase} _tc
*/
export const testUseAttribution = _tc => {
const d = delta.createTextDelta()
.insert('a')
.updateUsedAttribution('insert', ['me'])
.insert('b')
.insert('c', null, { insert: ['you'] })
.updateUsedAttribution('insert', null)
.insert('d')
.useAttribution({ insert: ['me'] })
.insert('e')
.useAttribution(null)
.insert('f')
.done()
const d2 = delta.createTextDelta()
.insert('a')
.insert('b', null, { insert: ['me'] })
.insert('c', null, { insert: ['you'] })
.insert('d')
.insert('e', null, { insert: ['me'] })
.insert('f')
.done()
t.compare(d, d2)
} }

View File

@@ -67,31 +67,6 @@ export const testFindTypeInOtherDoc = _tc => {
t.assert(findTypeInOtherYdoc(ytext, ydocClone) != null) t.assert(findTypeInOtherYdoc(ytext, ydocClone) != null)
} }
/**
* @param {t.TestCase} _tc
*/
export const testOriginInTransaction = _tc => {
const doc = new Y.Doc()
const ytext = doc.getText()
/**
* @type {Array<string>}
*/
const origins = []
doc.on('afterTransaction', (tr) => {
origins.push(tr.origin)
if (origins.length <= 1) {
ytext.toDelta(Y.snapshot(doc)) // adding a snapshot forces toDelta to create a cleanup transaction
doc.transact(() => {
ytext.insert(0, 'a')
}, 'nested')
}
})
doc.transact(() => {
ytext.insert(0, '0')
}, 'first')
t.compareArrays(origins, ['first', 'cleanup', 'nested'])
}
/** /**
* Client id should be changed when an instance receives updates from another client using the same client id. * Client id should be changed when an instance receives updates from another client using the same client id.
* *

View File

@@ -465,7 +465,7 @@ export const compare = users => {
const userArrayValues = users.map(u => u.getArray('array').toJSON()) const userArrayValues = users.map(u => u.getArray('array').toJSON())
const userMapValues = users.map(u => u.getMap('map').toJSON()) const userMapValues = users.map(u => u.getMap('map').toJSON())
const userXmlValues = users.map(u => u.get('xml', Y.XmlElement).toString()) const userXmlValues = users.map(u => u.get('xml', Y.XmlElement).toString())
const userTextValues = users.map(u => u.getText('text').toDelta()) const userTextValues = users.map(u => u.getText('text').getContent())
for (const u of users) { for (const u of users) {
t.assert(u.store.pendingDs === null) t.assert(u.store.pendingDs === null)
t.assert(u.store.pendingStructs === null) t.assert(u.store.pendingStructs === null)
@@ -490,7 +490,7 @@ export const compare = users => {
t.compare(userArrayValues[i], userArrayValues[i + 1]) t.compare(userArrayValues[i], userArrayValues[i + 1])
t.compare(userMapValues[i], userMapValues[i + 1]) t.compare(userMapValues[i], userMapValues[i + 1])
t.compare(userXmlValues[i], userXmlValues[i + 1]) t.compare(userXmlValues[i], userXmlValues[i + 1])
t.compare(userTextValues[i].map(/** @param {any} a */ a => typeof a.insert === 'string' ? a.insert : ' ').join('').length, users[i].getText('text').length) t.compare(userTextValues[i].ops.map(/** @param {any} a */ a => typeof a.insert === 'string' ? a.insert : ' ').join('').length, users[i].getText('text').length)
t.compare(userTextValues[i], userTextValues[i + 1], '', (_constructor, a, b) => { t.compare(userTextValues[i], userTextValues[i + 1], '', (_constructor, a, b) => {
if (a instanceof Y.AbstractType) { if (a instanceof Y.AbstractType) {
t.compare(a.toJSON(), b.toJSON()) t.compare(a.toJSON(), b.toJSON())

View File

@@ -1,6 +1,7 @@
import * as Y from '../src/index.js' import * as Y from '../src/index.js'
import { init } from './testHelper.js' // eslint-disable-line import { init } from './testHelper.js' // eslint-disable-line
import * as t from 'lib0/testing' import * as t from 'lib0/testing'
import * as delta from '../src/utils/Delta.js'
export const testInconsistentFormat = () => { export const testInconsistentFormat = () => {
/** /**
@@ -10,7 +11,7 @@ export const testInconsistentFormat = () => {
const content = /** @type {Y.XmlText} */ (ydoc.get('text', Y.XmlText)) const content = /** @type {Y.XmlText} */ (ydoc.get('text', Y.XmlText))
content.format(0, 6, { bold: null }) content.format(0, 6, { bold: null })
content.format(6, 4, { type: 'text' }) content.format(6, 4, { type: 'text' })
t.compare(content.toDelta(), [ t.compare(content.getContent(), delta.fromJSON([
{ {
attributes: { type: 'text' }, attributes: { type: 'text' },
insert: 'Merge Test' insert: 'Merge Test'
@@ -19,11 +20,10 @@ export const testInconsistentFormat = () => {
attributes: { type: 'text', italic: true }, attributes: { type: 'text', italic: true },
insert: ' After' insert: ' After'
} }
]) ]))
} }
const initializeYDoc = () => { const initializeYDoc = () => {
const yDoc = new Y.Doc({ gc: false }) const yDoc = new Y.Doc({ gc: false })
const content = /** @type {Y.XmlText} */ (yDoc.get('text', Y.XmlText)) const content = /** @type {Y.XmlText} */ (yDoc.get('text', Y.XmlText))
content.insert(0, ' After', { type: 'text', italic: true }) content.insert(0, ' After', { type: 'text', italic: true })
content.insert(0, 'Test', { type: 'text' }) content.insert(0, 'Test', { type: 'text' })
@@ -94,11 +94,11 @@ export const testUndoText = tc => {
t.assert(text0.toString() === 'bcxyz') t.assert(text0.toString() === 'bcxyz')
// test marks // test marks
text0.format(1, 3, { bold: true }) text0.format(1, 3, { bold: true })
t.compare(text0.toDelta(), [{ insert: 'b' }, { insert: 'cxy', attributes: { bold: true } }, { insert: 'z' }]) t.compare(text0.getContent(), delta.fromJSON([{ insert: 'b' }, { insert: 'cxy', attributes: { bold: true } }, { insert: 'z' }]))
undoManager.undo() undoManager.undo()
t.compare(text0.toDelta(), [{ insert: 'bcxyz' }]) t.compare(text0.getContent(), delta.fromJSON([{ insert: 'bcxyz' }]))
undoManager.redo() undoManager.redo()
t.compare(text0.toDelta(), [{ insert: 'b' }, { insert: 'cxy', attributes: { bold: true } }, { insert: 'z' }]) t.compare(text0.getContent(), delta.fromJSON([{ insert: 'b' }, { insert: 'cxy', attributes: { bold: true } }, { insert: 'z' }]))
} }
/** /**
@@ -686,16 +686,16 @@ export const testUndoDeleteTextFormat = _tc => {
undoManager.undo() undoManager.undo()
Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc)) Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc))
const expect = [ const expect = delta.fromJSON([
{ insert: 'Attack ships ' }, { insert: 'Attack ships ' },
{ {
insert: 'on fire', insert: 'on fire',
attributes: { bold: true } attributes: { bold: true }
}, },
{ insert: ' off the shoulder of Orion.' } { insert: ' off the shoulder of Orion.' }
] ])
t.compare(text.toDelta(), expect) t.compare(text.getContent(), expect)
t.compare(text2.toDelta(), expect) t.compare(text2.getContent(), expect)
} }
/** /**

View File

@@ -126,7 +126,7 @@ export const testKeyEncoding = tc => {
const update = Y.encodeStateAsUpdateV2(users[0]) const update = Y.encodeStateAsUpdateV2(users[0])
Y.applyUpdateV2(users[1], update) Y.applyUpdateV2(users[1], update)
t.compare(text1.toDelta(), [{ insert: 'c', attributes: { italic: true } }, { insert: 'b' }, { insert: 'a', attributes: { italic: true } }]) t.compare(text1.getContent().toJSON(), [{ insert: 'c', attributes: { italic: true } }, { insert: 'b' }, { insert: 'a', attributes: { italic: true } }])
compare(users) compare(users)
} }
@@ -331,7 +331,7 @@ export const testObfuscateUpdates = _tc => {
const omap = odoc.getMap('map') const omap = odoc.getMap('map')
const oarray = odoc.getArray('array') const oarray = odoc.getArray('array')
// test ytext // test ytext
const delta = otext.toDelta() const delta = /** @type {Array<any>} */ (otext.getContent().toJSON())
t.assert(delta.length === 2) t.assert(delta.length === 2)
t.assert(delta[0].insert !== 'text' && delta[0].insert.length === 4) t.assert(delta[0].insert !== 'text' && delta[0].insert.length === 4)
t.assert(object.length(delta[0].attributes) === 1) t.assert(object.length(delta[0].attributes) === 1)

View File

@@ -3,7 +3,7 @@ import * as t from 'lib0/testing'
import * as prng from 'lib0/prng' import * as prng from 'lib0/prng'
import * as math from 'lib0/math' import * as math from 'lib0/math'
import * as delta from '../src/utils/Delta.js' import * as delta from '../src/utils/Delta.js'
import { createIdMapFromIdSet, noAttributionsManager, TwosetAttributionManager } from 'yjs/internals' import { createIdMapFromIdSet, noAttributionsManager, TwosetAttributionManager, createAttributionManagerFromSnapshots } from 'yjs/internals'
const { init, compare } = Y const { init, compare } = Y
@@ -232,185 +232,138 @@ export const testDeltaBug = _tc => {
} }
] ]
ytext.applyDelta(addingList) ytext.applyDelta(addingList)
const result = ytext.toDelta() const result = ytext.getContent()
const expectedResult = [ const expectedResult = delta.createTextDelta()
{ .insert('\n', { 'block-id': 'block-28eea923-9cbb-4b6f-a950-cf7fd82bc087' })
attributes: { .insert('\n\n\n', { 'table-col': { width: '150' } })
'block-id': 'block-28eea923-9cbb-4b6f-a950-cf7fd82bc087' .insert('\n', {
}, 'block-id': 'block-9144be72-e528-4f91-b0b2-82d20408e9ea',
insert: '\n' 'table-cell-line': {
}, rowspan: '1',
{ colspan: '1',
attributes: {
'table-col': {
width: '150'
}
},
insert: '\n\n\n'
},
{
attributes: {
'block-id': 'block-9144be72-e528-4f91-b0b2-82d20408e9ea',
'table-cell-line': {
rowspan: '1',
colspan: '1',
row: 'row-6kv2ls',
cell: 'cell-apba4k'
},
row: 'row-6kv2ls', row: 'row-6kv2ls',
cell: 'cell-apba4k', cell: 'cell-apba4k'
rowspan: '1',
colspan: '1'
}, },
insert: '\n' row: 'row-6kv2ls',
}, cell: 'cell-apba4k',
{ rowspan: '1',
attributes: { colspan: '1'
'block-id': 'block-639adacb-1516-43ed-b272-937c55669a1c', })
'table-cell-line': { .insert('\n', {
rowspan: '1', 'block-id': 'block-639adacb-1516-43ed-b272-937c55669a1c',
colspan: '1', 'table-cell-line': {
row: 'row-6kv2ls', rowspan: '1',
cell: 'cell-a8qf0r' colspan: '1',
},
row: 'row-6kv2ls', row: 'row-6kv2ls',
cell: 'cell-a8qf0r', cell: 'cell-a8qf0r'
rowspan: '1',
colspan: '1'
}, },
insert: '\n' row: 'row-6kv2ls',
}, cell: 'cell-a8qf0r',
{ rowspan: '1',
attributes: { colspan: '1'
'block-id': 'block-6302ca4a-73a3-4c25-8c1e-b542f048f1c6', })
'table-cell-line': { .insert('\n', {
rowspan: '1', 'block-id': 'block-6302ca4a-73a3-4c25-8c1e-b542f048f1c6',
colspan: '1', 'table-cell-line': {
row: 'row-6kv2ls', rowspan: '1',
cell: 'cell-oi9ikb' colspan: '1',
},
row: 'row-6kv2ls', row: 'row-6kv2ls',
cell: 'cell-oi9ikb', cell: 'cell-oi9ikb'
rowspan: '1',
colspan: '1'
}, },
insert: '\n' row: 'row-6kv2ls',
}, cell: 'cell-oi9ikb',
{ rowspan: '1',
attributes: { colspan: '1'
'block-id': 'block-ceeddd05-330e-4f86-8017-4a3a060c4627', })
'table-cell-line': { .insert('\n', {
rowspan: '1', 'block-id': 'block-ceeddd05-330e-4f86-8017-4a3a060c4627',
colspan: '1', 'table-cell-line': {
row: 'row-d1sv2g', rowspan: '1',
cell: 'cell-dt6ks2' colspan: '1',
},
row: 'row-d1sv2g', row: 'row-d1sv2g',
cell: 'cell-dt6ks2', cell: 'cell-dt6ks2'
rowspan: '1',
colspan: '1'
}, },
insert: '\n' row: 'row-d1sv2g',
}, cell: 'cell-dt6ks2',
{ rowspan: '1',
attributes: { colspan: '1'
'block-id': 'block-37b19322-cb57-4e6f-8fad-0d1401cae53f', })
'table-cell-line': { .insert('\n', {
rowspan: '1', 'block-id': 'block-37b19322-cb57-4e6f-8fad-0d1401cae53f',
colspan: '1', 'table-cell-line': {
row: 'row-d1sv2g', rowspan: '1',
cell: 'cell-qah2ay' colspan: '1',
},
row: 'row-d1sv2g', row: 'row-d1sv2g',
cell: 'cell-qah2ay', cell: 'cell-qah2ay'
rowspan: '1',
colspan: '1'
}, },
insert: '\n' row: 'row-d1sv2g',
}, cell: 'cell-qah2ay',
{ rowspan: '1',
attributes: { colspan: '1'
'block-id': 'block-468a69b5-9332-450b-9107-381d593de249', })
'table-cell-line': { .insert('\n', {
rowspan: '1', 'block-id': 'block-468a69b5-9332-450b-9107-381d593de249',
colspan: '1', 'table-cell-line': {
row: 'row-d1sv2g', rowspan: '1',
cell: 'cell-fpcz5a' colspan: '1',
},
row: 'row-d1sv2g', row: 'row-d1sv2g',
cell: 'cell-fpcz5a', cell: 'cell-fpcz5a'
rowspan: '1',
colspan: '1'
}, },
insert: '\n' row: 'row-d1sv2g',
}, cell: 'cell-fpcz5a',
{ rowspan: '1',
attributes: { colspan: '1'
'block-id': 'block-26b1d252-9b2e-4808-9b29-04e76696aa3c', })
'table-cell-line': { .insert('\n', {
rowspan: '1', 'block-id': 'block-26b1d252-9b2e-4808-9b29-04e76696aa3c',
colspan: '1', 'table-cell-line': {
row: 'row-pflz90', rowspan: '1',
cell: 'cell-zrhylp' colspan: '1',
},
row: 'row-pflz90', row: 'row-pflz90',
cell: 'cell-zrhylp', cell: 'cell-zrhylp'
rowspan: '1',
colspan: '1'
}, },
insert: '\n' row: 'row-pflz90',
}, cell: 'cell-zrhylp',
{ rowspan: '1',
attributes: { colspan: '1'
'block-id': 'block-6af97ba7-8cf9-497a-9365-7075b938837b', })
'table-cell-line': { .insert('\n', {
rowspan: '1', 'block-id': 'block-6af97ba7-8cf9-497a-9365-7075b938837b',
colspan: '1', 'table-cell-line': {
row: 'row-pflz90', rowspan: '1',
cell: 'cell-s1q9nt' colspan: '1',
},
row: 'row-pflz90', row: 'row-pflz90',
cell: 'cell-s1q9nt', cell: 'cell-s1q9nt'
rowspan: '1',
colspan: '1'
}, },
insert: '\n' row: 'row-pflz90',
}, cell: 'cell-s1q9nt',
{ rowspan: '1',
insert: '\n', colspan: '1'
// This attributes has only list and no table-cell-line })
attributes: { // This attributes has only list and no table-cell-line
list: { .insert('\n', {
rowspan: '1', list: {
colspan: '1', rowspan: '1',
row: 'row-pflz90', colspan: '1',
cell: 'cell-20b0j9',
list: 'bullet'
},
'block-id': 'block-107e273e-86bc-44fd-b0d7-41ab55aca484',
row: 'row-pflz90', row: 'row-pflz90',
cell: 'cell-20b0j9', cell: 'cell-20b0j9',
rowspan: '1', list: 'bullet'
colspan: '1' },
} 'block-id': 'block-107e273e-86bc-44fd-b0d7-41ab55aca484',
}, row: 'row-pflz90',
cell: 'cell-20b0j9',
rowspan: '1',
colspan: '1'
})
// No table-cell-line below here // No table-cell-line below here
{ .insert('\n', {
attributes: { 'block-id': 'block-38161f9c-6f6d-44c5-b086-54cc6490f1e3'
'block-id': 'block-38161f9c-6f6d-44c5-b086-54cc6490f1e3' })
}, .insert('Content after table')
insert: '\n' .insert('\n', {
}, 'block-id': 'block-15630542-ef45-412d-9415-88f0052238ce'
{ })
insert: 'Content after table' .done()
},
{
attributes: {
'block-id': 'block-15630542-ef45-412d-9415-88f0052238ce'
},
insert: '\n'
}
]
t.compare(result, expectedResult) t.compare(result, expectedResult)
} }
@@ -477,11 +430,7 @@ export const testDeltaBug2 = _tc => {
insert: '\n', insert: '\n',
attributes: { 'block-id': 'block-8a1d2bb6-23c2-4bcf-af3c-3919ffea1697' } attributes: { 'block-id': 'block-8a1d2bb6-23c2-4bcf-af3c-3919ffea1697' }
}, },
{ insert: '\n\n', attributes: { 'table-col': { width: '150' } } }, { insert: '\n\n\n', attributes: { 'table-col': { width: '150' } } },
{
insert: '\n',
attributes: { 'table-col': { width: '150' } }
},
{ {
insert: '\n', insert: '\n',
attributes: { attributes: {
@@ -1640,8 +1589,8 @@ export const testDeltaBug2 = _tc => {
} }
] ]
ytext.applyDelta(changeEvent) ytext.applyDelta(changeEvent)
const delta = ytext.toDelta() const delta = ytext.getContent()
t.compare(delta[41], { t.compare(delta.ops[40].toJSON(), {
insert: '\n', insert: '\n',
attributes: { attributes: {
'block-id': 'block-9d6566a1-be55-4e20-999a-b990bc15e143' 'block-id': 'block-9d6566a1-be55-4e20-999a-b990bc15e143'
@@ -1667,8 +1616,8 @@ export const testDeltaAfterConcurrentFormatting = tc => {
*/ */
const deltas = [] const deltas = []
text1.observe(event => { text1.observe(event => {
if (event.delta.length > 0) { if (event.delta.ops.length > 0) {
deltas.push(event.delta) deltas.push(event.delta.toJSON())
} }
}) })
testConnector.flushAllMessages() testConnector.flushAllMessages()
@@ -1680,10 +1629,10 @@ export const testDeltaAfterConcurrentFormatting = tc => {
*/ */
export const testBasicInsertAndDelete = tc => { export const testBasicInsertAndDelete = tc => {
const { users, text0 } = init(tc, { users: 2 }) const { users, text0 } = init(tc, { users: 2 })
let delta let eventDelta
text0.observe(event => { text0.observe(event => {
delta = event.delta eventDelta = event.delta
}) })
text0.delete(0, 0) text0.delete(0, 0)
@@ -1691,21 +1640,21 @@ export const testBasicInsertAndDelete = tc => {
text0.insert(0, 'abc') text0.insert(0, 'abc')
t.assert(text0.toString() === 'abc', 'Basic insert works') t.assert(text0.toString() === 'abc', 'Basic insert works')
t.compare(delta, [{ insert: 'abc' }]) t.compare(eventDelta, delta.fromJSON([{ insert: 'abc' }]))
text0.delete(0, 1) text0.delete(0, 1)
t.assert(text0.toString() === 'bc', 'Basic delete works (position 0)') t.assert(text0.toString() === 'bc', 'Basic delete works (position 0)')
t.compare(delta, [{ delete: 1 }]) t.compare(eventDelta, delta.fromJSON([{ delete: 1 }]))
text0.delete(1, 1) text0.delete(1, 1)
t.assert(text0.toString() === 'b', 'Basic delete works (position 1)') t.assert(text0.toString() === 'b', 'Basic delete works (position 1)')
t.compare(delta, [{ retain: 1 }, { delete: 1 }]) t.compare(eventDelta, delta.fromJSON([{ retain: 1 }, { delete: 1 }]))
users[0].transact(() => { users[0].transact(() => {
text0.insert(0, '1') text0.insert(0, '1')
text0.delete(0, 1) text0.delete(0, 1)
}) })
t.compare(delta, []) t.compare(eventDelta, delta.fromJSON([]))
compare(users) compare(users)
} }
@@ -1715,36 +1664,36 @@ export const testBasicInsertAndDelete = tc => {
*/ */
export const testBasicFormat = tc => { export const testBasicFormat = tc => {
const { users, text0 } = init(tc, { users: 2 }) const { users, text0 } = init(tc, { users: 2 })
let delta let eventDelta
text0.observe(event => { text0.observe(event => {
delta = event.delta eventDelta = event.delta
}) })
text0.insert(0, 'abc', { bold: true }) text0.insert(0, 'abc', { bold: true })
t.assert(text0.toString() === 'abc', 'Basic insert with attributes works') t.assert(text0.toString() === 'abc', 'Basic insert with attributes works')
t.compare(text0.toDelta(), [{ insert: 'abc', attributes: { bold: true } }]) t.compare(text0.getContent(), delta.createTextDelta().insert('abc', { bold: true }).done())
t.compare(delta, [{ insert: 'abc', attributes: { bold: true } }]) t.compare(eventDelta, delta.createTextDelta().insert('abc', { bold: true }))
text0.delete(0, 1) text0.delete(0, 1)
t.assert(text0.toString() === 'bc', 'Basic delete on formatted works (position 0)') t.assert(text0.toString() === 'bc', 'Basic delete on formatted works (position 0)')
t.compare(text0.toDelta(), [{ insert: 'bc', attributes: { bold: true } }]) t.compare(text0.getContent(), delta.createTextDelta().insert('bc', { bold: true }))
t.compare(delta, [{ delete: 1 }]) t.compare(eventDelta, delta.createTextDelta().delete(1))
text0.delete(1, 1) text0.delete(1, 1)
t.assert(text0.toString() === 'b', 'Basic delete works (position 1)') t.assert(text0.toString() === 'b', 'Basic delete works (position 1)')
t.compare(text0.toDelta(), [{ insert: 'b', attributes: { bold: true } }]) t.compare(text0.getContent(), delta.createTextDelta().insert('b', { bold: true }))
t.compare(delta, [{ retain: 1 }, { delete: 1 }]) t.compare(eventDelta, delta.createTextDelta().retain(1).delete(1))
text0.insert(0, 'z', { bold: true }) text0.insert(0, 'z', { bold: true })
t.assert(text0.toString() === 'zb') t.assert(text0.toString() === 'zb')
t.compare(text0.toDelta(), [{ insert: 'zb', attributes: { bold: true } }]) t.compare(text0.getContent(), delta.createTextDelta().insert('zb', { bold: true }))
t.compare(delta, [{ insert: 'z', attributes: { bold: true } }]) t.compare(eventDelta, delta.createTextDelta().insert('z', { bold: true }))
// @ts-ignore // @ts-ignore
t.assert(text0._start.right.right.right.content.str === 'b', 'Does not insert duplicate attribute marker') t.assert(text0._start.right.right.right.content.str === 'b', 'Does not insert duplicate attribute marker')
text0.insert(0, 'y') text0.insert(0, 'y')
t.assert(text0.toString() === 'yzb') t.assert(text0.toString() === 'yzb')
t.compare(text0.toDelta(), [{ insert: 'y' }, { insert: 'zb', attributes: { bold: true } }]) t.compare(text0.getContent(), delta.createTextDelta().insert('y').insert('zb', { bold: true }))
t.compare(delta, [{ insert: 'y' }]) t.compare(eventDelta, delta.createTextDelta().insert('y'))
text0.format(0, 2, { bold: null }) text0.format(0, 2, { bold: null })
t.assert(text0.toString() === 'yzb') t.assert(text0.toString() === 'yzb')
t.compare(text0.toDelta(), [{ insert: 'yz' }, { insert: 'b', attributes: { bold: true } }]) t.compare(text0.getContent(), delta.createTextDelta().insert('yz').insert('b', { bold: true }))
t.compare(delta, [{ retain: 1 }, { retain: 1, attributes: { bold: null } }]) t.compare(eventDelta, delta.createTextDelta().retain(1).retain(1, { bold: null }))
compare(users) compare(users)
} }
@@ -1755,16 +1704,16 @@ export const testFalsyFormats = tc => {
const { users, text0 } = init(tc, { users: 2 }) const { users, text0 } = init(tc, { users: 2 })
let delta let delta
text0.observe(event => { text0.observe(event => {
delta = event.delta delta = event.delta.toJSON()
}) })
text0.insert(0, 'abcde', { falsy: false }) text0.insert(0, 'abcde', { falsy: false })
t.compare(text0.toDelta(), [{ insert: 'abcde', attributes: { falsy: false } }]) t.compare(text0.getContent().toJSON(), [{ insert: 'abcde', attributes: { falsy: false } }])
t.compare(delta, [{ insert: 'abcde', attributes: { falsy: false } }]) t.compare(delta, [{ insert: 'abcde', attributes: { falsy: false } }])
text0.format(1, 3, { falsy: true }) text0.format(1, 3, { falsy: true })
t.compare(text0.toDelta(), [{ insert: 'a', attributes: { falsy: false } }, { insert: 'bcd', attributes: { falsy: true } }, { insert: 'e', attributes: { falsy: false } }]) t.compare(text0.getContent().toJSON(), [{ insert: 'a', attributes: { falsy: false } }, { insert: 'bcd', attributes: { falsy: true } }, { insert: 'e', attributes: { falsy: false } }])
t.compare(delta, [{ retain: 1 }, { retain: 3, attributes: { falsy: true } }]) t.compare(delta, [{ retain: 1 }, { retain: 3, attributes: { falsy: true } }])
text0.format(2, 1, { falsy: false }) text0.format(2, 1, { falsy: false })
t.compare(text0.toDelta(), [{ insert: 'a', attributes: { falsy: false } }, { insert: 'b', attributes: { falsy: true } }, { insert: 'c', attributes: { falsy: false } }, { insert: 'd', attributes: { falsy: true } }, { insert: 'e', attributes: { falsy: false } }]) t.compare(text0.getContent().toJSON(), [{ insert: 'a', attributes: { falsy: false } }, { insert: 'b', attributes: { falsy: true } }, { insert: 'c', attributes: { falsy: false } }, { insert: 'd', attributes: { falsy: true } }, { insert: 'e', attributes: { falsy: false } }])
t.compare(delta, [{ retain: 2 }, { retain: 1, attributes: { falsy: false } }]) t.compare(delta, [{ retain: 2 }, { retain: 1, attributes: { falsy: false } }])
compare(users) compare(users)
} }
@@ -1783,7 +1732,7 @@ export const testMultilineFormat = _tc => {
{ retain: 1 }, // newline character { retain: 1 }, // newline character
{ retain: 10, attributes: { bold: true } } { retain: 10, attributes: { bold: true } }
]) ])
t.compare(testText.toDelta(), [ t.compare(testText.getContent().toJSON(), [
{ insert: 'Test', attributes: { bold: true } }, { insert: 'Test', attributes: { bold: true } },
{ insert: '\n' }, { insert: '\n' },
{ insert: 'Multi-line', attributes: { bold: true } }, { insert: 'Multi-line', attributes: { bold: true } },
@@ -1804,7 +1753,7 @@ export const testNotMergeEmptyLinesFormat = _tc => {
{ insert: '\nText' }, { insert: '\nText' },
{ insert: '\n', attributes: { title: true } } { insert: '\n', attributes: { title: true } }
]) ])
t.compare(testText.toDelta(), [ t.compare(testText.getContent().toJSON(), [
{ insert: 'Text' }, { insert: 'Text' },
{ insert: '\n', attributes: { title: true } }, { insert: '\n', attributes: { title: true } },
{ insert: '\nText' }, { insert: '\nText' },
@@ -1828,7 +1777,7 @@ export const testPreserveAttributesThroughDelete = _tc => {
{ delete: 1 }, { delete: 1 },
{ retain: 1, attributes: { title: true } } { retain: 1, attributes: { title: true } }
]) ])
t.compare(testText.toDelta(), [ t.compare(testText.getContent().toJSON(), [
{ insert: 'Text' }, { insert: 'Text' },
{ insert: '\n', attributes: { title: true } } { insert: '\n', attributes: { title: true } }
]) ])
@@ -1842,7 +1791,7 @@ export const testGetDeltaWithEmbeds = tc => {
text0.applyDelta([{ text0.applyDelta([{
insert: { linebreak: 's' } insert: { linebreak: 's' }
}]) }])
t.compare(text0.toDelta(), [{ t.compare(text0.getContent().toJSON(), [{
insert: { linebreak: 's' } insert: { linebreak: 's' }
}]) }])
} }
@@ -1855,18 +1804,18 @@ export const testTypesAsEmbed = tc => {
text0.applyDelta([{ text0.applyDelta([{
insert: new Y.Map([['key', 'val']]) insert: new Y.Map([['key', 'val']])
}]) }])
t.compare(text0.toDelta()[0].insert.toJSON(), { key: 'val' }) t.compare(/** @type {delta.InsertOp<any>} */ (text0.getContent().ops[0]).insert.toJSON(), { key: 'val' })
let firedEvent = false let firedEvent = false
text1.observe(event => { text1.observe(event => {
const d = event.delta const d = event.delta
t.assert(d.length === 1) t.assert(d.ops.length === 1)
t.compare(d.map(x => /** @type {Y.AbstractType<any>} */ (x.insert).toJSON()), [{ key: 'val' }]) t.compare(d.ops.map(x => /** @type {any} */ (x).insert.toJSON()), [{ key: 'val' }])
firedEvent = true firedEvent = true
}) })
testConnector.flushAllMessages() testConnector.flushAllMessages()
const delta = text1.toDelta() const delta = text1.getContent().toJSON()
t.assert(delta.length === 1) t.assert(delta.length === 1)
t.compare(delta[0].insert.toJSON(), { key: 'val' }) t.compare(/** @type {any} */ (delta[0]).insert.toJSON(), { key: 'val' })
t.assert(firedEvent, 'fired the event observer containing a Type-Embed') t.assert(firedEvent, 'fired the event observer containing a Type-Embed')
} }
@@ -1898,18 +1847,13 @@ export const testSnapshot = tc => {
}, { }, {
delete: 1 delete: 1
}]) }])
const state1 = text0.toDelta(snapshot1) const state1 = text0.getContent(createAttributionManagerFromSnapshots(snapshot1))
t.compare(state1, [{ insert: 'abcd' }]) t.compare(state1.toJSON(), [{ insert: 'abcd' }])
const state2 = text0.toDelta(snapshot2) const state2 = text0.getContent(createAttributionManagerFromSnapshots(snapshot2))
t.compare(state2, [{ insert: 'axcd' }]) t.compare(state2.toJSON(), [{ insert: 'axcd' }])
const state2Diff = text0.toDelta(snapshot2, snapshot1) const state2Diff = text0.getContent(createAttributionManagerFromSnapshots(snapshot1, snapshot2)).toJSON()
// @ts-ignore Remove userid info const expected = [{ insert: 'a' }, { insert: 'x', attribution: { insert: [] } }, { insert: 'b', attribution: { delete: [] } }, { insert: 'cd' }]
state2Diff.forEach(v => { t.compare(state2Diff, expected)
if (v.attributes && v.attributes.ychange) {
delete v.attributes.ychange.user
}
})
t.compare(state2Diff, [{ insert: 'a' }, { insert: 'x', attributes: { ychange: { type: 'added' } } }, { insert: 'b', attributes: { ychange: { type: 'removed' } } }, { insert: 'cd' }])
} }
/** /**
@@ -1928,8 +1872,8 @@ export const testSnapshotDeleteAfter = tc => {
}, { }, {
insert: 'e' insert: 'e'
}]) }])
const state1 = text0.toDelta(snapshot1) const state1 = text0.getContent(createAttributionManagerFromSnapshots(snapshot1, snapshot1))
t.compare(state1, [{ insert: 'abcd' }]) t.compare(state1, delta.fromJSON([{ insert: 'abcd' }]))
} }
/** /**
@@ -1948,7 +1892,7 @@ export const testToDeltaEmbedAttributes = tc => {
const { text0 } = init(tc, { users: 1 }) const { text0 } = init(tc, { users: 1 })
text0.insert(0, 'ab', { bold: true }) text0.insert(0, 'ab', { bold: true })
text0.insertEmbed(1, { image: 'imageSrc.png' }, { width: 100 }) text0.insertEmbed(1, { image: 'imageSrc.png' }, { width: 100 })
const delta0 = text0.toDelta() const delta0 = text0.getContent().toJSON()
t.compare(delta0, [{ insert: 'a', attributes: { bold: true } }, { insert: { image: 'imageSrc.png' }, attributes: { width: 100 } }, { insert: 'b', attributes: { bold: true } }]) t.compare(delta0, [{ insert: 'a', attributes: { bold: true } }, { insert: { image: 'imageSrc.png' }, attributes: { width: 100 } }, { insert: 'b', attributes: { bold: true } }])
} }
@@ -1959,7 +1903,7 @@ export const testToDeltaEmbedNoAttributes = tc => {
const { text0 } = init(tc, { users: 1 }) const { text0 } = init(tc, { users: 1 })
text0.insert(0, 'ab', { bold: true }) text0.insert(0, 'ab', { bold: true })
text0.insertEmbed(1, { image: 'imageSrc.png' }) text0.insertEmbed(1, { image: 'imageSrc.png' })
const delta0 = text0.toDelta() const delta0 = text0.getContent().toJSON()
t.compare(delta0, [{ insert: 'a', attributes: { bold: true } }, { insert: { image: 'imageSrc.png' } }, { insert: 'b', attributes: { bold: true } }], 'toDelta does not set attributes key when no attributes are present') t.compare(delta0, [{ insert: 'a', attributes: { bold: true } }, { insert: { image: 'imageSrc.png' } }, { insert: 'b', attributes: { bold: true } }], 'toDelta does not set attributes key when no attributes are present')
} }
@@ -2000,7 +1944,7 @@ export const testFormattingDeltaUnnecessaryAttributeChange = tc => {
}) })
testConnector.flushAllMessages() testConnector.flushAllMessages()
/** /**
* @type {Array<any>} * @type {Array<delta.TextDelta>}
*/ */
const deltas = [] const deltas = []
text0.observe(event => { text0.observe(event => {
@@ -2011,9 +1955,9 @@ export const testFormattingDeltaUnnecessaryAttributeChange = tc => {
}) })
text1.format(0, 1, { LIST_STYLES: 'number' }) text1.format(0, 1, { LIST_STYLES: 'number' })
testConnector.flushAllMessages() testConnector.flushAllMessages()
const filteredDeltas = deltas.filter(d => d.length > 0) const filteredDeltas = deltas.filter(d => d.ops.length > 0)
t.assert(filteredDeltas.length === 2) t.assert(filteredDeltas.length === 2)
t.compare(filteredDeltas[0], [ t.compare(filteredDeltas[0].toJSON(), [
{ retain: 1, attributes: { LIST_STYLES: 'number' } } { retain: 1, attributes: { LIST_STYLES: 'number' } }
]) ])
t.compare(filteredDeltas[0], filteredDeltas[1]) t.compare(filteredDeltas[0], filteredDeltas[1])
@@ -2267,9 +2211,9 @@ export const testFormattingBug = async _tc => {
{ insert: '\n', attributes: { url: 'http://docs.yjs.dev' } }, { insert: '\n', attributes: { url: 'http://docs.yjs.dev' } },
{ insert: '\n', attributes: { url: 'http://example.com' } } { insert: '\n', attributes: { url: 'http://example.com' } }
] ]
t.compare(text1.toDelta(), expectedResult) t.compare(text1.getContent().toJSON(), expectedResult)
t.compare(text1.toDelta(), text2.toDelta()) t.compare(text1.getContent().toJSON(), text2.getContent().toJSON())
console.log(text1.toDelta()) console.log(text1.getContent().toJSON())
} }
/** /**
@@ -2297,8 +2241,8 @@ export const testDeleteFormatting = _tc => {
{ insert: 'on ', attributes: { bold: true } }, { insert: 'on ', attributes: { bold: true } },
{ insert: 'fire off the shoulder of Orion.' } { insert: 'fire off the shoulder of Orion.' }
] ]
t.compare(text.toDelta(), expected) t.compare(text.getContent().toJSON(), expected)
t.compare(text2.toDelta(), expected) t.compare(text2.getContent().toJSON(), expected)
} }
/** /**
@@ -2590,13 +2534,13 @@ const checkResult = result => {
t.info('length of text = ' + result.users[i - 1].getText('text').length) t.info('length of text = ' + result.users[i - 1].getText('text').length)
t.measureTime('original toDelta perf', () => { t.measureTime('original toDelta perf', () => {
result.users[i - 1].getText('text').toDelta().map(typeToObject) result.users[i - 1].getText('text').getContent().toJSON().map(typeToObject)
}) })
t.measureTime('getContent(attributionManager) performance)', () => { t.measureTime('getContent(attributionManager) performance)', () => {
result.users[i - 1].getText('text').getContent() result.users[i - 1].getText('text').getContent()
}) })
const p1 = result.users[i - 1].getText('text').toDelta().map(typeToObject) const p1 = result.users[i - 1].getText('text').getContent().toJSON().map(typeToObject)
const p2 = result.users[i].getText('text').toDelta().map(typeToObject) const p2 = result.users[i].getText('text').getContent().toJSON().map(typeToObject)
t.compare(p1, p2) t.compare(p1, p2)
} }
// Uncomment this to find formatting-cleanup issues // Uncomment this to find formatting-cleanup issues
@@ -2613,7 +2557,7 @@ const checkResult = result => {
* @param {t.TestCase} tc * @param {t.TestCase} tc
*/ */
export const testAttributionManagerDefaultPerformance = tc => { export const testAttributionManagerDefaultPerformance = tc => {
const N = 10000 const N = 100000
const MaxDeletionLength = 5 // 25% chance of deletion const MaxDeletionLength = 5 // 25% chance of deletion
const MaxInsertionLength = 5 const MaxInsertionLength = 5
const ydoc = new Y.Doc() const ydoc = new Y.Doc()
@@ -2634,12 +2578,7 @@ export const testAttributionManagerDefaultPerformance = tc => {
const M = 100 const M = 100
t.measureTime(`original toString perf <executed ${M} times>`, () => { t.measureTime(`original toString perf <executed ${M} times>`, () => {
for (let i = 0; i < M; i++) { for (let i = 0; i < M; i++) {
ytext.toDelta() ytext.toString()
}
})
t.measureTime(`original toDelta perf <executed ${M} times>`, () => {
for (let i = 0; i < M; i++) {
ytext.toDelta()
} }
}) })
t.measureTime(`getContent(attributionManager) performance <executed ${M} times>`, () => { t.measureTime(`getContent(attributionManager) performance <executed ${M} times>`, () => {

View File

@@ -207,7 +207,7 @@ export const testFormattingBug = _tc => {
{ insert: 'C', attributes: { em: {}, strong: {} } } { insert: 'C', attributes: { em: {}, strong: {} } }
] ]
yxml.applyDelta(delta) yxml.applyDelta(delta)
t.compare(yxml.toDelta(), delta) t.compare(yxml.getContent().toJSON(), delta)
} }
/** /**
@@ -366,8 +366,8 @@ export const testElementAttributedContentViaDiffer = _tc => {
delta.createTextDelta().insert('bigworld') delta.createTextDelta().insert('bigworld')
]) ])
const attributedContent = yelement.getContentDeep(attributionManager) const attributedContent = yelement.getContentDeep(attributionManager)
console.log('children', JSON.stringify(attributedContent.children.toJSON().ops, null, 2)) console.log('children', JSON.stringify(attributedContent.children.toJSON(), null, 2))
console.log('cs expec', JSON.stringify(expectedContent.toJSON().ops, null, 2)) console.log('cs expec', JSON.stringify(expectedContent.toJSON(), null, 2))
console.log('attributes', attributedContent.attributes) console.log('attributes', attributedContent.attributes)
t.assert(attributedContent.children.equals(expectedContent)) t.assert(attributedContent.children.equals(expectedContent))
t.compare(attributedContent.attributes, { key: { prevValue: undefined, value: '42', attribution: null } }) t.compare(attributedContent.attributes, { key: { prevValue: undefined, value: '42', attribution: null } })