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

View File

@@ -24,7 +24,7 @@
"debug": "npm run gentesthtml && 0serve -o test.html",
"trace-deopt": "clear && node --trace-deopt ./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": {
".": {
@@ -85,7 +85,7 @@
},
"homepage": "https://docs.yjs.dev",
"dependencies": {
"lib0": "^0.2.105"
"lib0": "^0.2.107"
},
"devDependencies": {
"@y/protocols": "^1.0.6-1",

View File

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

View File

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

View File

@@ -7,7 +7,6 @@ import {
AbstractType,
getItemCleanStart,
getState,
isVisible,
createID,
YTextRefID,
callTypeObservers,
@@ -16,7 +15,6 @@ import {
GC,
ContentFormat,
ContentString,
splitSnapshotAffectedStructs,
iterateStructsByIdSet,
findMarker,
typeMapDelete,
@@ -26,7 +24,7 @@ import {
updateMarkerChanges,
ContentType,
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
} 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 () {
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 = {
keys: this.keys,
@@ -644,192 +642,106 @@ export class YTextEvent extends YEvent {
* Compute the changes in the delta format.
* 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
*/
get delta () {
if (this._delta === null) {
const y = /** @type {Doc} */ (this.target.doc)
const ydoc = /** @type {Doc} */ (this.target.doc)
const d = this._delta = delta.createTextDelta()
transact(ydoc, transaction => {
/**
* @type {Array<{insert?:string|object|AbstractType<any>, delete?:number, retain?:number, attributes?: Object<string,any>}>}
* @type {import('../utils/Delta.js').FormattingAttributes}
*/
const delta = []
transact(y, transaction => {
const currentAttributes = new Map() // saves all current attributes for insert
const oldAttributes = new Map()
let currentAttributes = {} // saves all current attributes for insert
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
/**
* @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) {
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) {
case ContentType:
case ContentEmbed:
if (this.adds(item)) {
if (!this.deletes(item)) {
addOp()
action = 'insert'
insert = item.content.getContent()[0]
addOp()
}
} else if (this.deletes(item)) {
if (action !== 'delete') {
addOp()
action = 'delete'
}
deleteLen += 1
if (freshInsert) {
d.usedAttributes = currentAttributes
usingCurrentAttributes = true
d.insert(item.content.getContent()[0])
} else if (freshDelete) {
d.delete(1)
} else if (!item.deleted) {
if (action !== 'retain') {
addOp()
action = 'retain'
}
retain += 1
d.usedAttributes = changedAttributes
usingChangedAttributes = true
d.retain(1)
}
break
case ContentString:
if (this.adds(item)) {
if (!this.deletes(item)) {
if (action !== 'insert') {
addOp()
action = 'insert'
}
insert += /** @type {ContentString} */ (item.content).str
}
} else if (this.deletes(item)) {
if (action !== 'delete') {
addOp()
action = 'delete'
}
deleteLen += item.length
if (freshInsert) {
d.usedAttributes = currentAttributes
usingCurrentAttributes = true
d.insert(/** @type {ContentString} */ (item.content).str)
} else if (freshDelete) {
d.delete(item.length)
} else if (!item.deleted) {
if (action !== 'retain') {
addOp()
action = 'retain'
}
retain += item.length
d.usedAttributes = changedAttributes
usingChangedAttributes = true
d.retain(item.length)
}
break
case ContentFormat: {
const { key, value } = /** @type {ContentFormat} */ (item.content)
if (this.adds(item)) {
if (!this.deletes(item)) {
const curVal = currentAttributes.get(key) ?? null
if (!equalAttrs(curVal, value)) {
if (action === 'retain') {
addOp()
const currAttrVal = currentAttributes[key] ?? null
if (freshDelete || freshInsert) {
// create fresh references
if (usingCurrentAttributes) {
currentAttributes = object.assign({}, currentAttributes)
usingCurrentAttributes = false
}
if (equalAttrs(value, (oldAttributes.get(key) ?? null))) {
delete attributes[key]
} else {
attributes[key] = value
if (usingChangedAttributes) {
usingChangedAttributes = false
changedAttributes = object.assign({}, changedAttributes)
}
} else if (value !== null) {
}
if (freshInsert) {
if (equalAttrs(value, currAttrVal)) {
item.delete(transaction)
} else if (equalAttrs(value, previousAttributes[key] ?? null)) {
delete currentAttributes[key]
delete changedAttributes[key]
} else {
currentAttributes[key] = value
changedAttributes[key] = value
}
}
} else if (this.deletes(item)) {
oldAttributes.set(key, value)
const curVal = currentAttributes.get(key) ?? null
if (!equalAttrs(curVal, value)) {
if (action === 'retain') {
addOp()
}
attributes[key] = curVal
}
} else if (freshDelete) {
changedAttributes[key] = currAttrVal
currentAttributes[key] = currAttrVal
previousAttributes[key] = value
} else if (!item.deleted) {
oldAttributes.set(key, value)
const attr = attributes[key]
if (attr !== undefined) {
if (!equalAttrs(attr, value)) {
if (action === 'retain') {
addOp()
// fresh reference to currentAttributes only
if (usingCurrentAttributes) {
currentAttributes = object.assign({}, currentAttributes)
usingCurrentAttributes = false
}
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 (action === 'insert') {
addOp()
}
updateCurrentAttributes(currentAttributes, /** @type {ContentFormat} */ (item.content))
currentAttributes[key] = value
previousAttributes[key] = value
}
break
}
}
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)
}
@@ -903,7 +815,7 @@ export class YText extends AbstractType {
*/
clone () {
const text = new YText()
text.applyDelta(this.toDelta())
text.applyDelta(this.getContent())
return text
}
@@ -957,7 +869,7 @@ export class YText extends AbstractType {
/**
* 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 {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 } = {}) {
if (this.doc !== null) {
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())
for (let i = 0; i < delta.length; i++) {
const op = delta[i]
for (let i = 0; i < deltaOps.length; i++) {
const op = deltaOps[i]
if (op.insert !== undefined) {
// Quill assumes that the content starts with an empty paragraph.
// Yjs/Y.Text assumes that it starts empty. We always hide that
// there is a newline at the end of the content.
// If we omit this step, clients will see a different number of
// 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) {
insertText(transaction, this, currPos, ins, op.attributes || {})
}
@@ -1006,8 +922,8 @@ export class YText extends AbstractType {
*/
getContentDeep (am = noAttributionsManager) {
return this.getContent(am).map(d =>
d instanceof delta.InsertOp && d.insert instanceof AbstractType
? new delta.InsertOp(d.insert.getContent(am), d.attributes, d.attribution)
d instanceof delta.InsertStringOp && d.insert instanceof AbstractType
? new delta.InsertStringOp(d.insert.getContent(am), d.attributes, d.attribution)
: d
)
}
@@ -1091,121 +1007,6 @@ export class YText extends AbstractType {
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.
*

View File

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

View File

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

View File

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

View File

@@ -5,10 +5,14 @@ import {
createDeleteSetFromStructStore,
createIdMapFromIdSet,
ContentDeleted,
Doc, Item, AbstractContent, IdMap, // eslint-disable-line
Snapshot, Doc, Item, AbstractContent, IdMap, // eslint-disable-line
insertIntoIdMap,
insertIntoIdSet,
diffIdMap
diffIdMap,
createIdMap,
createAttributionItem,
mergeIdMaps,
AttributionItem
} from '../internals.js'
import * as error from 'lib0/error'
@@ -90,6 +94,8 @@ export class AbstractAttributionManager {
readContent (_contents, _item) {
error.methodUnimplemented()
}
destroy () {}
}
/**
@@ -105,6 +111,8 @@ export class TwosetAttributionManager {
this.deletes = deletes
}
destroy () {}
/**
* @param {Array<AttributedContent<any>>} contents
* @param {Item} item
@@ -131,6 +139,8 @@ export class TwosetAttributionManager {
* @implements AbstractAttributionManager
*/
export class NoAttributionsManager {
destroy () {}
/**
* @param {Array<AttributedContent<any>>} contents
* @param {Item} item
@@ -243,3 +253,59 @@ export class DiffAttributionManager {
* @param {Doc} 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 fun from 'lib0/function'
import * as traits from 'lib0/traits'
import * as error from 'lib0/error'
/**
* @template {string|Array<any>|{[key: string]: any}} Content
* @typedef {InsertOp<Content>|RetainOp|DeleteOp} DeltaOp
* @template {any} ArrayContent
* @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 {
/**
* @param {Content} insert
* @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 {string} insert
* @param {FormattingAttributes|null} attributes
* @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 {DeltaJsonOp}
*/
toJSON () {
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) {
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 {DeltaJsonOp}
*/
toJSON () {
return { delete: this.delete }
}
@@ -86,6 +177,9 @@ export class RetainOp {
return this.retain
}
/**
* @return {DeltaJsonOp}
*/
toJSON () {
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 {{ array: ArrayDeltaContent, text: TextDeltaContent, custom: string|Array<any>|{[key:string]:any}}} DeltaTypeMapper
*/
/**
* @typedef {(TextDelta | ArrayDelta)} Delta
* @typedef {(TextDelta<any> | ArrayDelta<any>)} Delta
*/
/**
* @template {'array' | 'text' | 'custom'} Type
* @template {DeltaTypeMapper[Type]} [Content=DeltaTypeMapper[Type]]
* @template {DeltaOp<any,any>} TDeltaOp
*/
export class AbstractDelta {
/**
@@ -125,26 +211,26 @@ export class AbstractDelta {
constructor (type) {
this.type = type
/**
* @type {Array<DeltaOp<Content>>}
* @type {Array<TDeltaOp>}
*/
this.ops = []
}
/**
* @template {DeltaTypeMapper[Type]} MContent
* @param {(d:DeltaOp<Content>)=>DeltaOp<MContent>} f
* @return {DeltaBuilder<Type, MContent>}
* @template {(d:TDeltaOp) => DeltaOp<any,any>} Mapper
* @param {Mapper} f
* @return {DeltaBuilder<Type, Mapper extends (d:TDeltaOp) => infer OP ? OP : unknown>}
*/
map (f) {
const d = /** @type {DeltaBuilder<Type,any>} */ (new /** @type {any} */ (this.constructor)(this.type))
d.ops = this.ops.map(f)
// @ts-ignore
d._lastOp = d.ops[d.ops.length - 1] ?? null
d.lastOp = d.ops[d.ops.length - 1] ?? null
return d
}
/**
* @param {(d:DeltaOp<Content>,index:number)=>void} f
* @param {(d:TDeltaOp,index:number)=>void} f
*/
forEach (f) {
for (
@@ -157,22 +243,25 @@ export class AbstractDelta {
}
/**
* @param {AbstractDelta<Type, Content>} other
* @param {AbstractDelta<Type, TDeltaOp>} other
* @return {boolean}
*/
equals (other) {
return this[traits.EqualityTraitSymbol](other)
}
/**
* @returns {DeltaJson}
*/
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) {
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
*/
const mergeAttrs = (a, b) => {
const merged = a == null ? b : (b == null ? a : object.assign({}, a, b))
if (merged == null || object.isEmpty(merged)) { return null }
return merged
const merged = object.isEmpty(a) ? b : (object.isEmpty(b) ? a : object.assign({}, a, b))
return object.isEmpty(merged) ? null : merged
}
/**
* @template {'array' | 'text' | 'custom'} [Type='custom']
* @template {DeltaTypeMapper[Type]} [Content=DeltaTypeMapper[Type]]
* @extends AbstractDelta<Type,Content>
* @template {DeltaOp<any,any>} [TDeltaOp=DeltaOp<any,any>]
* @extends AbstractDelta<Type,TDeltaOp>
*/
export class DeltaBuilder extends AbstractDelta {
/**
@@ -209,10 +297,9 @@ export class DeltaBuilder extends AbstractDelta {
*/
this.usedAttribution = null
/**
* @private
* @type {DeltaOp<Content>?}
* @type {TDeltaOp?}
*/
this._lastOp = null
this.lastOp = null
}
/**
@@ -220,8 +307,44 @@ export class DeltaBuilder extends AbstractDelta {
* @return {this}
*/
useAttributes (attributes) {
if (this.usedAttributes === attributes) return this
this.usedAttributes = attributes && (object.isEmpty(attributes) ? null : object.assign({}, attributes))
this.usedAttributes = 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
}
@@ -229,32 +352,30 @@ export class DeltaBuilder extends AbstractDelta {
* @param {Attribution?} attribution
*/
useAttribution (attribution) {
if (this.usedAttribution === attribution) return this
this.usedAttribution = attribution && (object.isEmpty(attribution) ? null : object.assign({}, attribution))
this.usedAttribution = object.isEmpty(attribution) ? null : object.assign({}, attribution)
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 {Attribution?} attribution
* @return {this}
*/
insert (insert, attributes = null, attribution = null) {
const mergedAttributes = attributes == null ? this.usedAttributes : mergeAttrs(this.usedAttributes, attributes)
const mergedAttribution = attribution == null ? this.usedAttribution : mergeAttrs(this.usedAttribution, attribution)
if (this._lastOp instanceof InsertOp && (mergedAttributes === this._lastOp.attributes || fun.equalityDeep(mergedAttributes, this._lastOp.attributes)) && (mergedAttribution === this._lastOp.attribution || fun.equalityDeep(mergedAttribution, this._lastOp.attribution))) {
const mergedAttributes = mergeAttrs(this.usedAttributes, attributes)
const mergedAttribution = mergeAttrs(this.usedAttribution, 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) {
// @ts-ignore
this._lastOp.insert += insert
} else if (insert.constructor === Array && this._lastOp.insert.constructor === Array) {
// @ts-ignore
this._lastOp.insert.push(...insert)
this.lastOp.insert += insert
} else {
this.ops.push(this._lastOp = new InsertOp(insert, mergedAttributes, mergedAttribution))
// @ts-ignore
this.lastOp.insert.push(...insert)
}
} 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
}
@@ -268,10 +389,11 @@ export class DeltaBuilder extends AbstractDelta {
retain (retain, attributes = null, attribution = null) {
const mergedAttributes = mergeAttrs(this.usedAttributes, attributes)
const mergedAttribution = mergeAttrs(this.usedAttribution, attribution)
if (this._lastOp instanceof RetainOp && fun.equalityDeep(mergedAttributes, this._lastOp.attributes) && fun.equalityDeep(mergedAttribution, this._lastOp.attribution)) {
this._lastOp.retain += retain
if (this.lastOp instanceof RetainOp && fun.equalityDeep(mergedAttributes, this.lastOp.attributes) && fun.equalityDeep(mergedAttribution, this.lastOp.attribution)) {
this.lastOp.retain += retain
} 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
}
@@ -281,25 +403,30 @@ export class DeltaBuilder extends AbstractDelta {
* @return {this}
*/
delete (len) {
if (this._lastOp instanceof DeleteOp) {
this._lastOp.delete += len
if (this.lastOp instanceof DeleteOp) {
this.lastOp.delete += len
} else {
this.ops.push(this._lastOp = new DeleteOp(len))
// @ts-ignore
this.ops.push(this.lastOp = new DeleteOp(len))
}
return this
}
/**
* @return {AbstractDelta<Type,Content>}
* @return {AbstractDelta<Type,TDeltaOp>}
*/
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
}
}
/**
* @template {ArrayDeltaContent} [Content=ArrayDeltaContent]
* @extends DeltaBuilder<'array',Content>
* @template {any} ArrayContent
* @extends DeltaBuilder<'array', ArrayDeltaOp<ArrayContent>>>
*/
export class ArrayDelta extends DeltaBuilder {
constructor () {
@@ -308,8 +435,8 @@ export class ArrayDelta extends DeltaBuilder {
}
/**
* @template {TextDeltaContent} [Content=TextDeltaContent]
* @extends DeltaBuilder<'text',Content>
* @template {{ [key:string]: any }} Embeds
* @extends DeltaBuilder<'text',TextDeltaOp<Embeds>>
*/
export class TextDelta extends DeltaBuilder {
constructor () {
@@ -323,6 +450,28 @@ export class TextDelta extends DeltaBuilder {
export const createTextDelta = () => new TextDelta()
/**
* @return {ArrayDelta<ArrayDeltaContent>}
* @return {ArrayDelta<any>}
*/
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 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.'
/**
@@ -42,7 +50,7 @@ export class YEvent {
*/
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
/**
@@ -142,7 +150,7 @@ export class YEvent {
* 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.
*
* @type {Array<{insert?: string | Array<any> | object | AbstractType<any>, retain?: number, delete?: number, attributes?: Object<string, any>}>}
* @type {Delta}
*/
get delta () {
return this.changes.delta
@@ -166,7 +174,7 @@ export class YEvent {
* 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.
*
* @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 () {
let changes = this._changes

View File

@@ -157,23 +157,23 @@
"lib0/performance.js": "./node_modules/lib0/performance.js",
"lib0/dist/performance.cjs": "./node_modules/lib0/dist/performance.node.cjs",
"lib0/performance": "./node_modules/lib0/performance.js",
"y-protocols/package.json": "./node_modules/y-protocols/package.json",
"y-protocols/sync.js": "./node_modules/y-protocols/sync.js",
"y-protocols/dist/sync.cjs": "./node_modules/y-protocols/dist/sync.cjs",
"y-protocols/sync": "./node_modules/y-protocols/sync.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/awareness": "./node_modules/y-protocols/awareness.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/auth": "./node_modules/y-protocols/auth.js"
"@y/protocols/package.json": "./node_modules/@y/protocols/package.json",
"@y/protocols/sync.js": "./node_modules/@y/protocols/sync.js",
"@y/protocols/dist/sync.cjs": "./node_modules/@y/protocols/dist/sync.cjs",
"@y/protocols/sync": "./node_modules/@y/protocols/sync.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/awareness": "./node_modules/@y/protocols/awareness.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/auth": "./node_modules/@y/protocols/auth.js"
},
"scopes": {
"./node_modules/lib0/": {
"isomorphic.js": "./node_modules/isomorphic.js/browser.mjs",
"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": "./node_modules/lib0/index.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 => {
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([2])
.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)
}
/**
* @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.
*

View File

@@ -465,7 +465,7 @@ export const compare = users => {
const userArrayValues = users.map(u => u.getArray('array').toJSON())
const userMapValues = users.map(u => u.getMap('map').toJSON())
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) {
t.assert(u.store.pendingDs === null)
t.assert(u.store.pendingStructs === null)
@@ -490,7 +490,7 @@ export const compare = users => {
t.compare(userArrayValues[i], userArrayValues[i + 1])
t.compare(userMapValues[i], userMapValues[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) => {
if (a instanceof Y.AbstractType) {
t.compare(a.toJSON(), b.toJSON())

View File

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

View File

@@ -126,7 +126,7 @@ export const testKeyEncoding = tc => {
const update = Y.encodeStateAsUpdateV2(users[0])
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)
}
@@ -331,7 +331,7 @@ export const testObfuscateUpdates = _tc => {
const omap = odoc.getMap('map')
const oarray = odoc.getArray('array')
// test ytext
const delta = otext.toDelta()
const delta = /** @type {Array<any>} */ (otext.getContent().toJSON())
t.assert(delta.length === 2)
t.assert(delta[0].insert !== 'text' && delta[0].insert.length === 4)
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 math from 'lib0/math'
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
@@ -232,24 +232,11 @@ export const testDeltaBug = _tc => {
}
]
ytext.applyDelta(addingList)
const result = ytext.toDelta()
const expectedResult = [
{
attributes: {
'block-id': 'block-28eea923-9cbb-4b6f-a950-cf7fd82bc087'
},
insert: '\n'
},
{
attributes: {
'table-col': {
width: '150'
}
},
insert: '\n\n\n'
},
{
attributes: {
const result = ytext.getContent()
const expectedResult = delta.createTextDelta()
.insert('\n', { 'block-id': 'block-28eea923-9cbb-4b6f-a950-cf7fd82bc087' })
.insert('\n\n\n', { 'table-col': { width: '150' } })
.insert('\n', {
'block-id': 'block-9144be72-e528-4f91-b0b2-82d20408e9ea',
'table-cell-line': {
rowspan: '1',
@@ -261,11 +248,8 @@ export const testDeltaBug = _tc => {
cell: 'cell-apba4k',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
})
.insert('\n', {
'block-id': 'block-639adacb-1516-43ed-b272-937c55669a1c',
'table-cell-line': {
rowspan: '1',
@@ -277,11 +261,8 @@ export const testDeltaBug = _tc => {
cell: 'cell-a8qf0r',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
})
.insert('\n', {
'block-id': 'block-6302ca4a-73a3-4c25-8c1e-b542f048f1c6',
'table-cell-line': {
rowspan: '1',
@@ -293,11 +274,8 @@ export const testDeltaBug = _tc => {
cell: 'cell-oi9ikb',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
})
.insert('\n', {
'block-id': 'block-ceeddd05-330e-4f86-8017-4a3a060c4627',
'table-cell-line': {
rowspan: '1',
@@ -309,11 +287,8 @@ export const testDeltaBug = _tc => {
cell: 'cell-dt6ks2',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
})
.insert('\n', {
'block-id': 'block-37b19322-cb57-4e6f-8fad-0d1401cae53f',
'table-cell-line': {
rowspan: '1',
@@ -325,11 +300,8 @@ export const testDeltaBug = _tc => {
cell: 'cell-qah2ay',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
})
.insert('\n', {
'block-id': 'block-468a69b5-9332-450b-9107-381d593de249',
'table-cell-line': {
rowspan: '1',
@@ -341,11 +313,8 @@ export const testDeltaBug = _tc => {
cell: 'cell-fpcz5a',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
})
.insert('\n', {
'block-id': 'block-26b1d252-9b2e-4808-9b29-04e76696aa3c',
'table-cell-line': {
rowspan: '1',
@@ -357,11 +326,8 @@ export const testDeltaBug = _tc => {
cell: 'cell-zrhylp',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
attributes: {
})
.insert('\n', {
'block-id': 'block-6af97ba7-8cf9-497a-9365-7075b938837b',
'table-cell-line': {
rowspan: '1',
@@ -373,13 +339,9 @@ export const testDeltaBug = _tc => {
cell: 'cell-s1q9nt',
rowspan: '1',
colspan: '1'
},
insert: '\n'
},
{
insert: '\n',
})
// This attributes has only list and no table-cell-line
attributes: {
.insert('\n', {
list: {
rowspan: '1',
colspan: '1',
@@ -392,25 +354,16 @@ export const testDeltaBug = _tc => {
cell: 'cell-20b0j9',
rowspan: '1',
colspan: '1'
}
},
})
// No table-cell-line below here
{
attributes: {
.insert('\n', {
'block-id': 'block-38161f9c-6f6d-44c5-b086-54cc6490f1e3'
},
insert: '\n'
},
{
insert: 'Content after table'
},
{
attributes: {
})
.insert('Content after table')
.insert('\n', {
'block-id': 'block-15630542-ef45-412d-9415-88f0052238ce'
},
insert: '\n'
}
]
})
.done()
t.compare(result, expectedResult)
}
@@ -477,11 +430,7 @@ export const testDeltaBug2 = _tc => {
insert: '\n',
attributes: { 'block-id': 'block-8a1d2bb6-23c2-4bcf-af3c-3919ffea1697' }
},
{ insert: '\n\n', attributes: { 'table-col': { width: '150' } } },
{
insert: '\n',
attributes: { 'table-col': { width: '150' } }
},
{ insert: '\n\n\n', attributes: { 'table-col': { width: '150' } } },
{
insert: '\n',
attributes: {
@@ -1640,8 +1589,8 @@ export const testDeltaBug2 = _tc => {
}
]
ytext.applyDelta(changeEvent)
const delta = ytext.toDelta()
t.compare(delta[41], {
const delta = ytext.getContent()
t.compare(delta.ops[40].toJSON(), {
insert: '\n',
attributes: {
'block-id': 'block-9d6566a1-be55-4e20-999a-b990bc15e143'
@@ -1667,8 +1616,8 @@ export const testDeltaAfterConcurrentFormatting = tc => {
*/
const deltas = []
text1.observe(event => {
if (event.delta.length > 0) {
deltas.push(event.delta)
if (event.delta.ops.length > 0) {
deltas.push(event.delta.toJSON())
}
})
testConnector.flushAllMessages()
@@ -1680,10 +1629,10 @@ export const testDeltaAfterConcurrentFormatting = tc => {
*/
export const testBasicInsertAndDelete = tc => {
const { users, text0 } = init(tc, { users: 2 })
let delta
let eventDelta
text0.observe(event => {
delta = event.delta
eventDelta = event.delta
})
text0.delete(0, 0)
@@ -1691,21 +1640,21 @@ export const testBasicInsertAndDelete = tc => {
text0.insert(0, 'abc')
t.assert(text0.toString() === 'abc', 'Basic insert works')
t.compare(delta, [{ insert: 'abc' }])
t.compare(eventDelta, delta.fromJSON([{ insert: 'abc' }]))
text0.delete(0, 1)
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)
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(() => {
text0.insert(0, '1')
text0.delete(0, 1)
})
t.compare(delta, [])
t.compare(eventDelta, delta.fromJSON([]))
compare(users)
}
@@ -1715,36 +1664,36 @@ export const testBasicInsertAndDelete = tc => {
*/
export const testBasicFormat = tc => {
const { users, text0 } = init(tc, { users: 2 })
let delta
let eventDelta
text0.observe(event => {
delta = event.delta
eventDelta = event.delta
})
text0.insert(0, 'abc', { bold: true })
t.assert(text0.toString() === 'abc', 'Basic insert with attributes works')
t.compare(text0.toDelta(), [{ insert: 'abc', attributes: { bold: true } }])
t.compare(delta, [{ insert: 'abc', attributes: { bold: true } }])
t.compare(text0.getContent(), delta.createTextDelta().insert('abc', { bold: true }).done())
t.compare(eventDelta, delta.createTextDelta().insert('abc', { bold: true }))
text0.delete(0, 1)
t.assert(text0.toString() === 'bc', 'Basic delete on formatted works (position 0)')
t.compare(text0.toDelta(), [{ insert: 'bc', attributes: { bold: true } }])
t.compare(delta, [{ delete: 1 }])
t.compare(text0.getContent(), delta.createTextDelta().insert('bc', { bold: true }))
t.compare(eventDelta, delta.createTextDelta().delete(1))
text0.delete(1, 1)
t.assert(text0.toString() === 'b', 'Basic delete works (position 1)')
t.compare(text0.toDelta(), [{ insert: 'b', attributes: { bold: true } }])
t.compare(delta, [{ retain: 1 }, { delete: 1 }])
t.compare(text0.getContent(), delta.createTextDelta().insert('b', { bold: true }))
t.compare(eventDelta, delta.createTextDelta().retain(1).delete(1))
text0.insert(0, 'z', { bold: true })
t.assert(text0.toString() === 'zb')
t.compare(text0.toDelta(), [{ insert: 'zb', attributes: { bold: true } }])
t.compare(delta, [{ insert: 'z', attributes: { bold: true } }])
t.compare(text0.getContent(), delta.createTextDelta().insert('zb', { bold: true }))
t.compare(eventDelta, delta.createTextDelta().insert('z', { bold: true }))
// @ts-ignore
t.assert(text0._start.right.right.right.content.str === 'b', 'Does not insert duplicate attribute marker')
text0.insert(0, 'y')
t.assert(text0.toString() === 'yzb')
t.compare(text0.toDelta(), [{ insert: 'y' }, { insert: 'zb', attributes: { bold: true } }])
t.compare(delta, [{ insert: 'y' }])
t.compare(text0.getContent(), delta.createTextDelta().insert('y').insert('zb', { bold: true }))
t.compare(eventDelta, delta.createTextDelta().insert('y'))
text0.format(0, 2, { bold: null })
t.assert(text0.toString() === 'yzb')
t.compare(text0.toDelta(), [{ insert: 'yz' }, { insert: 'b', attributes: { bold: true } }])
t.compare(delta, [{ retain: 1 }, { retain: 1, attributes: { bold: null } }])
t.compare(text0.getContent(), delta.createTextDelta().insert('yz').insert('b', { bold: true }))
t.compare(eventDelta, delta.createTextDelta().retain(1).retain(1, { bold: null }))
compare(users)
}
@@ -1755,16 +1704,16 @@ export const testFalsyFormats = tc => {
const { users, text0 } = init(tc, { users: 2 })
let delta
text0.observe(event => {
delta = event.delta
delta = event.delta.toJSON()
})
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 } }])
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 } }])
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 } }])
compare(users)
}
@@ -1783,7 +1732,7 @@ export const testMultilineFormat = _tc => {
{ retain: 1 }, // newline character
{ retain: 10, attributes: { bold: true } }
])
t.compare(testText.toDelta(), [
t.compare(testText.getContent().toJSON(), [
{ insert: 'Test', attributes: { bold: true } },
{ insert: '\n' },
{ insert: 'Multi-line', attributes: { bold: true } },
@@ -1804,7 +1753,7 @@ export const testNotMergeEmptyLinesFormat = _tc => {
{ insert: '\nText' },
{ insert: '\n', attributes: { title: true } }
])
t.compare(testText.toDelta(), [
t.compare(testText.getContent().toJSON(), [
{ insert: 'Text' },
{ insert: '\n', attributes: { title: true } },
{ insert: '\nText' },
@@ -1828,7 +1777,7 @@ export const testPreserveAttributesThroughDelete = _tc => {
{ delete: 1 },
{ retain: 1, attributes: { title: true } }
])
t.compare(testText.toDelta(), [
t.compare(testText.getContent().toJSON(), [
{ insert: 'Text' },
{ insert: '\n', attributes: { title: true } }
])
@@ -1842,7 +1791,7 @@ export const testGetDeltaWithEmbeds = tc => {
text0.applyDelta([{
insert: { linebreak: 's' }
}])
t.compare(text0.toDelta(), [{
t.compare(text0.getContent().toJSON(), [{
insert: { linebreak: 's' }
}])
}
@@ -1855,18 +1804,18 @@ export const testTypesAsEmbed = tc => {
text0.applyDelta([{
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
text1.observe(event => {
const d = event.delta
t.assert(d.length === 1)
t.compare(d.map(x => /** @type {Y.AbstractType<any>} */ (x.insert).toJSON()), [{ key: 'val' }])
t.assert(d.ops.length === 1)
t.compare(d.ops.map(x => /** @type {any} */ (x).insert.toJSON()), [{ key: 'val' }])
firedEvent = true
})
testConnector.flushAllMessages()
const delta = text1.toDelta()
const delta = text1.getContent().toJSON()
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')
}
@@ -1898,18 +1847,13 @@ export const testSnapshot = tc => {
}, {
delete: 1
}])
const state1 = text0.toDelta(snapshot1)
t.compare(state1, [{ insert: 'abcd' }])
const state2 = text0.toDelta(snapshot2)
t.compare(state2, [{ insert: 'axcd' }])
const state2Diff = text0.toDelta(snapshot2, snapshot1)
// @ts-ignore Remove userid info
state2Diff.forEach(v => {
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' }])
const state1 = text0.getContent(createAttributionManagerFromSnapshots(snapshot1))
t.compare(state1.toJSON(), [{ insert: 'abcd' }])
const state2 = text0.getContent(createAttributionManagerFromSnapshots(snapshot2))
t.compare(state2.toJSON(), [{ insert: 'axcd' }])
const state2Diff = text0.getContent(createAttributionManagerFromSnapshots(snapshot1, snapshot2)).toJSON()
const expected = [{ insert: 'a' }, { insert: 'x', attribution: { insert: [] } }, { insert: 'b', attribution: { delete: [] } }, { insert: 'cd' }]
t.compare(state2Diff, expected)
}
/**
@@ -1928,8 +1872,8 @@ export const testSnapshotDeleteAfter = tc => {
}, {
insert: 'e'
}])
const state1 = text0.toDelta(snapshot1)
t.compare(state1, [{ insert: 'abcd' }])
const state1 = text0.getContent(createAttributionManagerFromSnapshots(snapshot1, snapshot1))
t.compare(state1, delta.fromJSON([{ insert: 'abcd' }]))
}
/**
@@ -1948,7 +1892,7 @@ export const testToDeltaEmbedAttributes = tc => {
const { text0 } = init(tc, { users: 1 })
text0.insert(0, 'ab', { bold: true })
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 } }])
}
@@ -1959,7 +1903,7 @@ export const testToDeltaEmbedNoAttributes = tc => {
const { text0 } = init(tc, { users: 1 })
text0.insert(0, 'ab', { bold: true })
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')
}
@@ -2000,7 +1944,7 @@ export const testFormattingDeltaUnnecessaryAttributeChange = tc => {
})
testConnector.flushAllMessages()
/**
* @type {Array<any>}
* @type {Array<delta.TextDelta>}
*/
const deltas = []
text0.observe(event => {
@@ -2011,9 +1955,9 @@ export const testFormattingDeltaUnnecessaryAttributeChange = tc => {
})
text1.format(0, 1, { LIST_STYLES: 'number' })
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.compare(filteredDeltas[0], [
t.compare(filteredDeltas[0].toJSON(), [
{ retain: 1, attributes: { LIST_STYLES: 'number' } }
])
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://example.com' } }
]
t.compare(text1.toDelta(), expectedResult)
t.compare(text1.toDelta(), text2.toDelta())
console.log(text1.toDelta())
t.compare(text1.getContent().toJSON(), expectedResult)
t.compare(text1.getContent().toJSON(), text2.getContent().toJSON())
console.log(text1.getContent().toJSON())
}
/**
@@ -2297,8 +2241,8 @@ export const testDeleteFormatting = _tc => {
{ insert: 'on ', attributes: { bold: true } },
{ insert: 'fire off the shoulder of Orion.' }
]
t.compare(text.toDelta(), expected)
t.compare(text2.toDelta(), expected)
t.compare(text.getContent().toJSON(), 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.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)', () => {
result.users[i - 1].getText('text').getContent()
})
const p1 = result.users[i - 1].getText('text').toDelta().map(typeToObject)
const p2 = result.users[i].getText('text').toDelta().map(typeToObject)
const p1 = result.users[i - 1].getText('text').getContent().toJSON().map(typeToObject)
const p2 = result.users[i].getText('text').getContent().toJSON().map(typeToObject)
t.compare(p1, p2)
}
// Uncomment this to find formatting-cleanup issues
@@ -2613,7 +2557,7 @@ const checkResult = result => {
* @param {t.TestCase} tc
*/
export const testAttributionManagerDefaultPerformance = tc => {
const N = 10000
const N = 100000
const MaxDeletionLength = 5 // 25% chance of deletion
const MaxInsertionLength = 5
const ydoc = new Y.Doc()
@@ -2634,12 +2578,7 @@ export const testAttributionManagerDefaultPerformance = tc => {
const M = 100
t.measureTime(`original toString perf <executed ${M} times>`, () => {
for (let i = 0; i < M; i++) {
ytext.toDelta()
}
})
t.measureTime(`original toDelta perf <executed ${M} times>`, () => {
for (let i = 0; i < M; i++) {
ytext.toDelta()
ytext.toString()
}
})
t.measureTime(`getContent(attributionManager) performance <executed ${M} times>`, () => {

View File

@@ -207,7 +207,7 @@ export const testFormattingBug = _tc => {
{ insert: 'C', attributes: { em: {}, strong: {} } }
]
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')
])
const attributedContent = yelement.getContentDeep(attributionManager)
console.log('children', JSON.stringify(attributedContent.children.toJSON().ops, null, 2))
console.log('cs expec', JSON.stringify(expectedContent.toJSON().ops, null, 2))
console.log('children', JSON.stringify(attributedContent.children.toJSON(), null, 2))
console.log('cs expec', JSON.stringify(expectedContent.toJSON(), null, 2))
console.log('attributes', attributedContent.attributes)
t.assert(attributedContent.children.equals(expectedContent))
t.compare(attributedContent.attributes, { key: { prevValue: undefined, value: '42', attribution: null } })