Text
Loro supports both plain text and rich text. When rich text features (like mark and unmark) are not used, the text container operates as plain text without any rich text overhead, making it efficient for simple text operations.
LoroText offers excellent performance, particularly when handling large strings. It significantly outperforms native JavaScript string operations due to its internal B-tree structure. All basic operations like insert and delete have O(log N) time complexity, making it highly efficient even when working with documents containing several million characters.
To learn how rich text CRDT in Loro works under the hood, please refer to our blog: Introduction to Loro's Rich Text CRDT.
Rich Text Config
To use rich text in Loro, you need to specify the expanding behaviors for each format first. When we insert new text at the format boundaries, they define whether the inserted text should inherit the format.
There are four kinds of expansion behaviors:
after
(default): when inserting text right after the given range, the mark will be expanded to include the inserted textbefore
: when inserting text right before the given range, the mark will be expanded to include the inserted textnone
: the mark will not be expanded to include the inserted text at the boundariesboth
: when inserting text either right before or right after the given range, the mark will be expanded to include the inserted text
Example
const doc = new LoroDoc();
doc.configTextStyle({
bold: { expand: "after" },
link: { expand: "before" },
});
const text = doc.getText("text");
text.insert(0, "Hello World!");
text.mark({ start: 0, end: 5 }, "bold", true);
expect(text.toDelta()).toStrictEqual([
{
insert: "Hello",
attributes: {
bold: true,
},
},
{
insert: " World!",
},
] as Delta<string>[]);
// " Test" will inherit the bold style because `bold` is configured to expand forward
text.insert(5, " Test");
expect(text.toDelta()).toStrictEqual([
{
insert: "Hello Test",
attributes: {
bold: true,
},
},
{
insert: " World!",
},
] as Delta<string>[]);
Methods
insert(pos: number, s: string)
Insert text at the given pos.
delete(pos: number, len: number)
Delete text at the given range.
slice(start: number, end: number): string
Get a string slice.
toString(): string
Get the plain text value.
charAt(pos: number): char
Get the character at the given position.
splice(pos: number, len: number, text: string): string
Delete and return the string at the given range and insert a string at the same position.
length(): number
Get the length of text
getCursor(pos: number, side: Side): Cursor | undefined
Get the cursor at the given position.
toDelta(): Delta<string>[]
Get the rich text value. It's in Quill's Delta format (opens in a new tab).
mark(range: {start: number, end: number}, key: string, value: any): void
Mark the given range with a key-value pair.
unmark(range: {start: number, end: number}, key: string): void
Remove key-value pairs in the given range with the given key.
update(text: string)
Update the current text based on the provided text.
applyDelta(delta: Delta<string>[]): void
Change the state of this text by delta.
If a delta item is insert
, it should include all the attributes of the
inserted text. Loro's rich text CRDT may make the inserted text inherit some
styles when you use the insert
method directly. However, when you use
applyDelta
if some attributes are inherited from CRDT but not included in the
delta, they will be removed.
Another special property of applyDelta
is if you format an attribute for
ranges out of the text length, Loro will insert new lines to fill the gap first.
It's useful when you build the binding between Loro and rich text editors like
Quill, which might assume there is always a newline at the end of the text
implicitly.
const doc = new LoroDoc();
const text = doc.getText("text");
doc.configTextStyle({ bold: { expand: "after" } });
text.insert(0, "Hello World!");
text.mark({ start: 0, end: 5 }, "bold", true);
const delta = text.toDelta();
const text2 = doc.getText("text2");
text2.applyDelta(delta);
expect(text2.toDelta()).toStrictEqual(delta);
subscribe(f: (event: Listener)): number
This method returns a number that can be used to remove the subscription.
The text event is in Delta<string>[]
format. It can be used to bind the rich
text editor. It has the same type as the arg of applyDelta
, so the following
example works:
(async () => {
const doc1 = new LoroDoc();
doc1.configTextStyle({
link: { expand: "none" },
bold: { expand: "after" },
});
const text1 = doc1.getText("text");
const doc2 = new LoroDoc();
doc2.configTextStyle({
link: { expand: "none" },
bold: { expand: "after" },
});
const text2 = doc2.getText("text");
text1.subscribe((event) => {
const e = event.diff as TextDiff;
text2.applyDelta(e.diff);
});
text1.insert(0, "foo");
text1.mark({ start: 0, end: 3 }, "link", true);
doc1.commit();
await new Promise((r) => setTimeout(r, 1));
expect(text2.toDelta()).toStrictEqual(text1.toDelta());
text1.insert(3, "baz");
doc1.commit();
await new Promise((r) => setTimeout(r, 1));
expect(text2.toDelta()).toStrictEqual([
{ insert: "foo", attributes: { link: true } },
{ insert: "baz" },
]);
expect(text2.toDelta()).toStrictEqual(text1.toDelta());
text1.mark({ start: 2, end: 5 }, "bold", true);
doc1.commit();
await new Promise((r) => setTimeout(r, 1));
expect(text2.toDelta()).toStrictEqual(text1.toDelta());
})();