mirror of
https://github.com/yjs/yjs.git
synced 2026-05-03 03:00:41 -04:00
478 lines
12 KiB
JavaScript
478 lines
12 KiB
JavaScript
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 {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
|
|
*/
|
|
|
|
/**
|
|
* @typedef {import('./AttributionManager.js').Attribution} Attribution
|
|
*/
|
|
|
|
/**
|
|
* @typedef {{ [key: string]: any }} FormattingAttributes
|
|
*/
|
|
|
|
/**
|
|
* @typedef {Array<DeltaJsonOp>} DeltaJson
|
|
*/
|
|
|
|
/**
|
|
* @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
|
|
*/
|
|
constructor (insert, attributes, attribution) {
|
|
this.insert = insert
|
|
this.attributes = attributes
|
|
this.attribution = attribution
|
|
}
|
|
|
|
get length () {
|
|
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 {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)
|
|
}
|
|
}
|
|
|
|
export class DeleteOp {
|
|
/**
|
|
* @param {number} len
|
|
*/
|
|
constructor (len) {
|
|
this.delete = len
|
|
}
|
|
|
|
get length () {
|
|
return 0
|
|
}
|
|
|
|
/**
|
|
* @return {DeltaJsonOp}
|
|
*/
|
|
toJSON () {
|
|
return { delete: this.delete }
|
|
}
|
|
|
|
/**
|
|
* @param {DeleteOp} other
|
|
*/
|
|
[traits.EqualityTraitSymbol] (other) {
|
|
return this.delete === other.delete
|
|
}
|
|
}
|
|
|
|
export class RetainOp {
|
|
/**
|
|
* @param {number} retain
|
|
* @param {FormattingAttributes|null} attributes
|
|
* @param {Attribution|null} attribution
|
|
*/
|
|
constructor (retain, attributes, attribution) {
|
|
this.retain = retain
|
|
this.attributes = attributes
|
|
this.attribution = attribution
|
|
}
|
|
|
|
get length () {
|
|
return this.retain
|
|
}
|
|
|
|
/**
|
|
* @return {DeltaJsonOp}
|
|
*/
|
|
toJSON () {
|
|
return object.assign({ retain: this.retain }, this.attributes ? { attributes: this.attributes } : {}, this.attribution ? { attribution: this.attribution } : {})
|
|
}
|
|
|
|
/**
|
|
* @param {RetainOp} other
|
|
*/
|
|
[traits.EqualityTraitSymbol] (other) {
|
|
return this.retain === other.retain && fun.equalityDeep(this.attributes, other.attributes) && fun.equalityDeep(this.attribution, other.attribution)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @typedef {string | { [key: string]: any }} TextDeltaContent
|
|
*/
|
|
|
|
/**
|
|
* @typedef {(TextDelta<any> | ArrayDelta<any>)} Delta
|
|
*/
|
|
|
|
/**
|
|
* @template {'array' | 'text' | 'custom'} Type
|
|
* @template {DeltaOp<any,any>} TDeltaOp
|
|
*/
|
|
export class AbstractDelta {
|
|
/**
|
|
* @param {Type} type
|
|
*/
|
|
constructor (type) {
|
|
this.type = type
|
|
/**
|
|
* @type {Array<TDeltaOp>}
|
|
*/
|
|
this.ops = []
|
|
}
|
|
|
|
/**
|
|
* @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
|
|
return d
|
|
}
|
|
|
|
/**
|
|
* @param {(d:TDeltaOp,index:number)=>void} f
|
|
*/
|
|
forEach (f) {
|
|
for (
|
|
let i = 0, index = 0, op = this.ops[i];
|
|
i < this.ops.length;
|
|
i++, index += op.length, op = this.ops[i]
|
|
) {
|
|
f(op, index)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param {AbstractDelta<Type, TDeltaOp>} other
|
|
* @return {boolean}
|
|
*/
|
|
equals (other) {
|
|
return this[traits.EqualityTraitSymbol](other)
|
|
}
|
|
|
|
/**
|
|
* @returns {DeltaJson}
|
|
*/
|
|
toJSON () {
|
|
return this.ops.map(o => o.toJSON())
|
|
}
|
|
|
|
/**
|
|
* @param {AbstractDelta<Type,TDeltaOp>} other
|
|
*/
|
|
[traits.EqualityTraitSymbol] (other) {
|
|
return fun.equalityDeep(this.ops, other.ops)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper function to merge attribution and attributes. The latter input "wins".
|
|
*
|
|
* @template {{ [key: string]: any }} T
|
|
* @param {T | null} a
|
|
* @param {T | null} b
|
|
*/
|
|
const mergeAttrs = (a, b) => {
|
|
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 {DeltaOp<any,any>} [TDeltaOp=DeltaOp<any,any>]
|
|
* @extends AbstractDelta<Type,TDeltaOp>
|
|
*/
|
|
export class DeltaBuilder extends AbstractDelta {
|
|
/**
|
|
* @param {Type} type
|
|
*/
|
|
constructor (type) {
|
|
super(type)
|
|
/**
|
|
* @type {FormattingAttributes?}
|
|
*/
|
|
this.usedAttributes = null
|
|
/**
|
|
* @type {Attribution?}
|
|
*/
|
|
this.usedAttribution = null
|
|
/**
|
|
* @type {TDeltaOp?}
|
|
*/
|
|
this.lastOp = null
|
|
}
|
|
|
|
/**
|
|
* @param {FormattingAttributes?} attributes
|
|
* @return {this}
|
|
*/
|
|
useAttributes (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
|
|
}
|
|
|
|
/**
|
|
* @param {Attribution?} attribution
|
|
*/
|
|
useAttribution (attribution) {
|
|
this.usedAttribution = object.isEmpty(attribution) ? null : object.assign({}, attribution)
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* @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 = 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 {
|
|
// @ts-ignore
|
|
this.lastOp.insert.push(...insert)
|
|
}
|
|
} else {
|
|
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
|
|
}
|
|
|
|
/**
|
|
* @param {number} retain
|
|
* @param {FormattingAttributes?} attributes
|
|
* @param {Attribution?} attribution
|
|
* @return {this}
|
|
*/
|
|
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
|
|
} else {
|
|
// @ts-ignore
|
|
this.ops.push(this.lastOp = new RetainOp(retain, mergedAttributes, mergedAttribution))
|
|
}
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* @param {number} len
|
|
* @return {this}
|
|
*/
|
|
delete (len) {
|
|
if (this.lastOp instanceof DeleteOp) {
|
|
this.lastOp.delete += len
|
|
} else {
|
|
// @ts-ignore
|
|
this.ops.push(this.lastOp = new DeleteOp(len))
|
|
}
|
|
return this
|
|
}
|
|
|
|
/**
|
|
* @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 {any} ArrayContent
|
|
* @extends DeltaBuilder<'array', ArrayDeltaOp<ArrayContent>>>
|
|
*/
|
|
export class ArrayDelta extends DeltaBuilder {
|
|
constructor () {
|
|
super('array')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @template {{ [key:string]: any }} Embeds
|
|
* @extends DeltaBuilder<'text',TextDeltaOp<Embeds>>
|
|
*/
|
|
export class TextDelta extends DeltaBuilder {
|
|
constructor () {
|
|
super('text')
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @return {TextDelta<TextDeltaContent>}
|
|
*/
|
|
export const createTextDelta = () => new TextDelta()
|
|
|
|
/**
|
|
* @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()
|
|
}
|