import { Result } from '@kizahasi/result'; import { diff_match_patch } from 'diff-match-patch'; import { r, i, d, insert$, delete$, retain, ComposeAndTransformError, transform as transformCore, PositiveInt, Operation, OperationBuilder, mapOperation, } from '@kizahasi/ot-core'; import { NonEmptyString } from '../nonEmptyString'; import { twoWayFactory } from '../operationBuilderFactory'; export type TwoWayOperation = Operation; export type TwoWayOperationUnit = | { t: typeof r; r: number; } | { t: typeof i; i: string; } | { t: typeof d; d: string; }; export const diff = ({ prevState, nextState, }: { prevState: string; nextState: string; }): TwoWayOperation => { const builder = new OperationBuilder(twoWayFactory); const dmp = new diff_match_patch(); dmp.diff_main(prevState, nextState).forEach(([diffType, diff]) => { switch (diffType) { case -1: builder.delete(new NonEmptyString(diff)); break; case 0: builder.retain(new PositiveInt(diff.length)); break; case 1: builder.insert(new NonEmptyString(diff)); break; } }); return builder.build(); }; export const transform = ({ first, second, }: { first: TwoWayOperation; second: TwoWayOperation; }): Result< { firstPrime: TwoWayOperation; secondPrime: TwoWayOperation }, ComposeAndTransformError > => { const result = transformCore({ first: Array.from(new OperationBuilder(twoWayFactory, first).toUnits()), second: Array.from(new OperationBuilder(twoWayFactory, second).toUnits()), factory: twoWayFactory, splitDelete: (target, deleteCount) => [ new NonEmptyString(target.value.substring(0, deleteCount.value)), new NonEmptyString(target.value.substring(deleteCount.value)), ], }); if (result.isError) { return Result.error({ ...result.error, first, second, }); } return result; }; export const toUnit = (source: TwoWayOperation): TwoWayOperationUnit[] => { return Array.from(new OperationBuilder(twoWayFactory, source).toUnits()).map(unit => { switch (unit.type) { case insert$: return { t: i, i: unit.insert.value, } as const; case delete$: return { t: d, d: unit.delete.value, } as const; case retain: return { t: r, r: unit.retain.value, } as const; } }); }; export const ofUnit = (source: ReadonlyArray): TwoWayOperation => { const builder = new OperationBuilder(twoWayFactory); for (const unit of source) { if (unit == null) { continue; } switch (unit.t) { case r: { const retain = unit.r; const retainAsPositiveInt = PositiveInt.tryCreate(retain); if (retainAsPositiveInt == null) { continue; } builder.retain(retainAsPositiveInt); break; } case i: { const insert = unit.i; const insertAsNonEpmtyString = NonEmptyString.tryCreate(insert); if (insertAsNonEpmtyString == null) { continue; } builder.insert(insertAsNonEpmtyString); break; } case d: { const del = unit.d; const delAsNonEmptyString = NonEmptyString.tryCreate(del); if (delAsNonEmptyString == null) { continue; } builder.delete(delAsNonEmptyString); break; } } } return builder.build(); }; export const toUpOperation = (source: TwoWayOperation): Operation => { return mapOperation({ source, mapInsert: insert => insert, mapDelete: del => del.length, }); }; export const toDownOperation = ( source: TwoWayOperation ): Operation => { return mapOperation({ source, mapInsert: insert => insert.length, mapDelete: del => del, }); };