Docs
Tutorial
Text

Text

To learn how it 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 expanding behaviors:

  • after(default): when inserting text right after the given range, the mark will be expanded to include the inserted text
  • before: when inserting text right before the given range, the mark will be expanded to include the inserted text
  • none: the mark will not be expanded to include the inserted text at the boundaries
  • both: 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 Loro();
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.

toString(): string

Get the plain text value.

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.

applyDelta(delta: Delta<string>[]): void

Change the state of this text by delta.

If a delta item is inserte, 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.

import { Loro } from "loro-crdt";
const doc = new Loro();
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(loro: Loro, 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 Loro();
  doc1.configTextStyle({
    link: { expand: "none" },
    bold: { expand: "after" },
  })
  const text1 = doc1.getText("text");
  const doc2 = new Loro();
  doc2.configTextStyle({
    link: { expand: "none" },
    bold: { expand: "after" },
  })
  const text2 = doc2.getText("text");
  text1.subscribe(doc1, (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());
}