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|InsertArrayOp|RetainOp|DeleteOp} DeltaOp */ /** * @template {{[key: string]: any}} Embeds * @typedef {InsertStringOp|InsertEmbedOp|RetainOp|DeleteOp} TextDeltaOp */ /** * @template {any} ArrayContent * @typedef {InsertArrayOp|RetainOp|DeleteOp} ArrayDeltaOp */ /** * @typedef {import('./AttributionManager.js').Attribution} Attribution */ /** * @typedef {{ [key: string]: any }} FormattingAttributes */ /** * @typedef {Array} 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} 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} 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} 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 | ArrayDelta)} Delta */ /** * @template {'array' | 'text' | 'custom'} Type * @template {DeltaOp} TDeltaOp */ export class AbstractDelta { /** * @param {Type} type */ constructor (type) { this.type = type /** * @type {Array} */ this.ops = [] } /** * @template {(d:TDeltaOp) => DeltaOp} Mapper * @param {Mapper} f * @return {DeltaBuilder infer OP ? OP : unknown>} */ map (f) { const d = /** @type {DeltaBuilder} */ (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} other * @return {boolean} */ equals (other) { return this[traits.EqualityTraitSymbol](other) } /** * @returns {DeltaJson} */ toJSON () { return this.ops.map(o => o.toJSON()) } /** * @param {AbstractDelta} 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} [TDeltaOp=DeltaOp] * @extends AbstractDelta */ 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 ? string | Embeds : never) | (TDeltaOp extends InsertArrayOp ? Array : 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} */ 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>> */ export class ArrayDelta extends DeltaBuilder { constructor () { super('array') } } /** * @template {{ [key:string]: any }} Embeds * @extends DeltaBuilder<'text',TextDeltaOp> */ export class TextDelta extends DeltaBuilder { constructor () { super('text') } } /** * @return {TextDelta} */ export const createTextDelta = () => new TextDelta() /** * @return {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() }