@monstermann/delta
v0.3.0
Published
Functional operational-transform.
Readme
Functional operational-transform.
Differences from quill-delta
This library has been largely ported from quill-delta, some differences:
- Immutable with optional transient mutations
- Functional data-first/data-last API acting upon plain arrays
- Support for nested attributes has been removed
- Cloning is only done when the data actually changes
- Deep-cloning has been replaced with shallow-cloning
- Around 50% smaller
Example
import { Delta } from "@monstermann/delta";
// Create a document
const doc = Delta.insert([], "Hello world");
// Create a change that makes "Hello" bold
const change = Delta.retain([], 5, { bold: true });
// Apply the change
const result = Delta.compose(doc, change);
// [{ insert: "Hello", attributes: { bold: true } },
// { insert: " world" }]
// Compute the difference between two documents
const a = Delta.insert([], "Hello");
const b = Delta.insert([], "Hello world");
Delta.diff(a, b);
// [{ retain: 5 },
// { insert: " world" }]Installation
npm install @monstermann/deltapnpm add @monstermann/deltayarn add @monstermann/deltabun add @monstermann/deltaDelta
batch
function Delta.batch(
ops: Delta,
transform: (delta: Delta) => Delta,
): DeltaBatches multiple delta operations together for improved performance.
Example
import { Delta } from "@monstermann/delta";
Delta.batch([], (delta) => {
// First change copies:
delta = Delta.insert(delta, "Hello", { bold: true });
// Other changes mutate:
delta = Delta.insert(delta, " world");
return delta;
});
// [{ insert: "Hello", attributes: { bold: true } },
// { insert: " world" }]import { Delta } from "@monstermann/delta";
pipe(
[],
Delta.batch((delta) => {
// First change copies:
delta = Delta.insert(delta, "Hello", { bold: true });
// Other changes mutate:
delta = Delta.insert(delta, " world");
return delta;
}),
);
// [{ insert: "Hello", attributes: { bold: true } },
// { insert: " world" }]chop
function Delta.chop(ops: Delta): DeltaRemoves a trailing retain operation if it has no attributes.
Example
import { Delta } from "@monstermann/delta";
Delta.chop(pipe(
[],
Delta.insert("Hello"),
Delta.retain(5)
));
// [{ insert: "Hello" }]
Delta.chop(pipe(
[],
Delta.insert("Hello"),
Delta.retain(5, { bold: true })
));
// [{ insert: "Hello" },
// { retain: 5, attributes: { bold: true } }]import { Delta } from "@monstermann/delta";
pipe(
[],
Delta.insert("Hello"),
Delta.retain(5),
Delta.chop()
);
// [{ insert: "Hello" }]clean
function Delta.clean(ops: Delta): DeltaNormalizes the delta by merging consecutive operations of the same type and attributes.
Example
import { Delta } from "@monstermann/delta";
Delta.clean(pipe(
[],
Delta.insert("Hello"),
Delta.insert(" world")
));
// [{ insert: "Hello world" }]
Delta.clean(
pipe(
[],
Delta.insert("Hello", { bold: true }),
Delta.insert(" world", { bold: true }),
),
);
// [{ insert: "Hello world", attributes: { bold: true } }]import { Delta } from "@monstermann/delta";
pipe(
[],
Delta.insert("Hello"),
Delta.insert(" world"),
Delta.clean()
);
// [{ insert: "Hello world" }]compose
function Delta.compose(a: Delta, b: Delta): DeltaComposes two deltas into a single delta that represents applying a then b.
Example
import { Delta } from "@monstermann/delta";
const a = Delta.insert([], "Hello");
const b = pipe(
[],
Delta.retain(5),
Delta.insert(" world")
);
Delta.compose(a, b);
// [{ insert: "Hello world" }]
const format = Delta.retain([], 5, { bold: true });
Delta.compose(a, format);
// [{ insert: "Hello", attributes: { bold: true } }]import { Delta } from "@monstermann/delta";
const a = Delta.insert([], "Hello");
const b = pipe(
[],
Delta.retain(5),
Delta.insert(" world")
);
pipe(a, Delta.compose(b));
// [{ insert: "Hello world" }]concat
function Delta.concat(a: Delta, b: Delta): DeltaConcatenates two deltas together, merging adjacent operations if possible.
Example
import { Delta } from "@monstermann/delta";
const a = Delta.insert([], "Hello");
const b = Delta.insert([], " world");
Delta.concat(a, b);
// [{ insert: "Hello world" }]
const bold = Delta.insert([], "Hello", { bold: true });
const italic = Delta.insert([], " world", { italic: true });
Delta.concat(bold, italic);
// [{ insert: "Hello", attributes: { bold: true } },
// { insert: " world", attributes: { italic: true } }]import { Delta } from "@monstermann/delta";
const a = Delta.insert([], "Hello");
const b = Delta.insert([], " world");
pipe(a, Delta.concat(b));
// [{ insert: "Hello world" }]diff
function Delta.diff(a: Delta, b: Delta, cursor?: number): DeltaComputes the difference between two document deltas, returning a delta that transforms a into b.
The optional cursor parameter provides a hint about where the user's cursor is positioned. This helps produce more intuitive diffs when there are multiple valid ways to represent the same change.
Example
import { Delta } from "@monstermann/delta";
const a = Delta.insert([], "Hello");
const b = Delta.insert([], "Hello world");
Delta.diff(a, b);
// [{ retain: 5 },
// { insert: " world" }]
const plain = Delta.insert([], "Hello");
const bold = Delta.insert([], "Hello", { bold: true });
Delta.diff(plain, bold);
// [{ retain: 5, attributes: { bold: true } }]import { Delta } from "@monstermann/delta";
const a = Delta.insert([], "Hello");
const b = Delta.insert([], "Hello world");
pipe(a, Delta.diff(b));
// [{ retain: 5 },
// { insert: " world" }]Cursor hint
When text changes are ambiguous, the cursor position determines where the change is placed:
import { Delta } from "@monstermann/delta";
const a = Delta.insert([], "foo");
const b = Delta.insert([], "foo bar foo");
// cursor=3: user typed " bar foo" at the end
Delta.diff(a, b, 3);
// [{ retain: 3 },
// { insert: " bar foo" }]
// cursor=0: user typed "foo bar " at the beginning
Delta.diff(a, b, 0);
// [{ insert: "foo bar " }]equals
function Delta.equals(a: Delta, b: Delta): booleanChecks if two deltas are equal by comparing their operations and attributes.
Example
import { Delta } from "@monstermann/delta";
const a = Delta.insert([], "Hello", { bold: true });
const b = Delta.insert([], "Hello", { bold: true });
const c = Delta.insert([], "Hello", { italic: true });
Delta.equals(a, b); // true
Delta.equals(a, c); // falseimport { Delta } from "@monstermann/delta";
const a = Delta.insert([], "Hello", { bold: true });
const b = Delta.insert([], "Hello", { bold: true });
pipe(a, Delta.equals(b)); // trueinsert
function Delta.insert(
ops: Delta,
content: string | EmbedValue,
attributes?: OpAttributes | null,
): DeltaAdds an insert operation to the delta.
Example
import { Delta } from "@monstermann/delta";
Delta.insert([], "Hello");
// [{ insert: "Hello" }]
Delta.insert([], "Hello", { bold: true });
// [{ insert: "Hello", attributes: { bold: true } }]import { Delta } from "@monstermann/delta";
pipe([], Delta.insert("Hello"));
// [{ insert: "Hello" }]
pipe(
[],
Delta.insert("Hello", { bold: true }),
Delta.insert(" world", { italic: true }),
);
// [{ insert: "Hello", attributes: { bold: true } },
// { insert: " world", attributes: { italic: true } }]invert
function Delta.invert(a: Delta, b: Delta): DeltaReturns the inverse of a delta against a base document. Applying the inverted delta undoes the original change.
Example
import { Delta } from "@monstermann/delta";
const base = Delta.insert([], "Hello");
const change = Delta.retain([], 5, { bold: true });
Delta.invert(change, base);
// [{ retain: 5, attributes: { bold: null } }]
const insert = pipe(
[],
Delta.retain(5),
Delta.insert(" world")
);
Delta.invert(insert, base);
// [{ retain: 5 },
// { delete: 6 }]import { Delta } from "@monstermann/delta";
const base = Delta.insert([], "Hello");
const change = Delta.retain([], 5, { bold: true });
pipe(change, Delta.invert(base));
// [{ retain: 5, attributes: { bold: null } }]length
function Delta.length(ops: Delta): numberReturns the total length of the delta (sum of all operation lengths).
Example
import { Delta } from "@monstermann/delta";
Delta.length(Delta.insert([], "Hello")); // 5
Delta.length(pipe(
[],
Delta.insert("Hello"),
Delta.retain(3),
Delta.remove(2)
)); // 10import { Delta } from "@monstermann/delta";
pipe([], Delta.insert("Hello"), Delta.length()); // 5
pipe(
[],
Delta.insert("Hello"),
Delta.retain(3),
Delta.remove(2),
Delta.length(),
); // 10push
function Delta.push(ops: Delta, op: Op): DeltaPushes an operation onto the delta, merging with the previous operation if possible.
Example
import { Delta } from "@monstermann/delta";
Delta.push([], { insert: "Hello" });
// [{ insert: "Hello" }]
Delta.push(Delta.push([], { insert: "Hello" }), { insert: " world" });
// [{ insert: "Hello world" }]import { Delta } from "@monstermann/delta";
pipe([], Delta.push({ insert: "Hello" }));
// [{ insert: "Hello" }]
pipe([], Delta.push({ insert: "Hello" }), Delta.push({ insert: " world" }));
// [{ insert: "Hello world" }]remove
function Delta.remove(ops: Delta, length: number): DeltaAdds a remove operation to the delta.
Example
import { Delta } from "@monstermann/delta";
Delta.remove([], 5);
// [{ delete: 5 }]import { Delta } from "@monstermann/delta";
pipe([], Delta.remove(5));
// [{ delete: 5 }]
pipe([], Delta.retain(3), Delta.remove(5));
// [{ retain: 3 },
// { delete: 5 }]retain
function Delta.retain(
ops: Delta,
length: number,
attributes?: OpAttributes | null,
): DeltaAdds a retain operation to the delta, optionally with attributes to apply formatting.
Example
import { Delta } from "@monstermann/delta";
Delta.retain([], 5);
// [{ retain: 5 }]
Delta.retain([], 5, { bold: true });
// [{ retain: 5, attributes: { bold: true } }]import { Delta } from "@monstermann/delta";
pipe([], Delta.retain(5));
// [{ retain: 5 }]
pipe(
[],
Delta.retain(3),
Delta.retain(2, { italic: true })
);
// [{ retain: 3 },
// { retain: 2, attributes: { italic: true } }]Removing attributes
Use null to remove an attribute when composing deltas:
import { Delta } from "@monstermann/delta";
const doc = Delta.insert([], "Hello", { bold: true });
// [{ insert: "Hello", attributes: { bold: true } }]
const removeBold = Delta.retain([], 5, { bold: null });
// [{ retain: 5, attributes: { bold: null } }]
Delta.compose(doc, removeBold);
// [{ insert: "Hello" }]slice
function Delta.slice(ops: Delta, start: number, end?: number): DeltaReturns a portion of the delta from start to end.
Example
import { Delta } from "@monstermann/delta";
const delta = Delta.insert([], "Hello world");
Delta.slice(delta, 0, 5);
// [{ insert: "Hello" }]
Delta.slice(delta, 6);
// [{ insert: "world" }]
const formatted = pipe(
[],
Delta.insert("Hello", { bold: true }),
Delta.insert(" world", { italic: true }),
);
Delta.slice(formatted, 3, 8);
// [{ insert: "lo", attributes: { bold: true } },
// { insert: " wo", attributes: { italic: true } }]import { Delta } from "@monstermann/delta";
pipe(
[],
Delta.insert("Hello world"),
Delta.slice(0, 5)
);
// [{ insert: "Hello" }]
pipe(
[],
Delta.insert("Hello world"),
Delta.slice(6)
);
// [{ insert: "world" }]transform
function Delta.transform(
a: Delta,
b: Delta,
priority?: boolean,
): DeltaTransforms delta b to account for delta a having been applied first. When both deltas insert at the same position, priority determines which insert comes first.
Example
import { Delta } from "@monstermann/delta";
const a = Delta.insert([], "Hello");
const b = Delta.insert([], "World");
Delta.transform(a, b, true);
// [{ retain: 5 },
// { insert: "World" }]
Delta.transform(a, b, false);
// [{ insert: "World" }]
const format = Delta.retain([], 5, { bold: true });
const insert = pipe(
[],
Delta.retain(2),
Delta.insert("XXX")
);
Delta.transform(insert, format);
// [{ retain: 8, attributes: { bold: true } }]import { Delta } from "@monstermann/delta";
const a = Delta.insert([], "Hello");
const b = Delta.insert([], "World");
pipe(a, Delta.transform(b, true));
// [{ retain: 5 },
// { insert: "World" }]Op
length
function Op.length(op: Op): numberReturns the length of a single operation.
For string inserts this is the number of characters. For embed inserts this is always 1. For retain and remove operations this is the numeric value.
Example
import { Op } from "@monstermann/delta";
Op.length({ insert: "Hello" }); // 5
Op.length({ insert: { image: "..." } }); // 1
Op.length({ retain: 3 }); // 3
Op.length({ delete: 2 }); // 2