Merge pull request #807 from AppFlowy-IO/refactor/text-delta

Fix: emoji position and unicode issues
This commit is contained in:
Vincent Chan 2022-08-10 21:33:09 +08:00 committed by GitHub
commit 7ac5f822c9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 351 additions and 202 deletions

View File

@ -219,7 +219,5 @@ class TextNode extends Node {
delta: delta ?? this.delta, delta: delta ?? this.delta,
); );
// TODO: It's unneccesry to compute everytime. String toRawString() => _delta.toRawString();
String toRawString() =>
_delta.operations.whereType<TextInsert>().map((op) => op.content).join();
} }

View File

@ -257,8 +257,10 @@ TextOperation? _textOperationFromJson(Map<String, dynamic> json) {
} }
// basically copy from: https://github.com/quilljs/delta // basically copy from: https://github.com/quilljs/delta
class Delta { class Delta extends Iterable<TextOperation> {
final List<TextOperation> operations; final List<TextOperation> _operations;
String? _rawString;
List<int>? _runeIndexes;
factory Delta.fromJson(List<dynamic> list) { factory Delta.fromJson(List<dynamic> list) {
final operations = <TextOperation>[]; final operations = <TextOperation>[];
@ -273,51 +275,50 @@ class Delta {
return Delta(operations); return Delta(operations);
} }
Delta([List<TextOperation>? ops]) : operations = ops ?? <TextOperation>[]; Delta([List<TextOperation>? ops]) : _operations = ops ?? <TextOperation>[];
Delta addAll(List<TextOperation> textOps) { void addAll(Iterable<TextOperation> textOps) {
textOps.forEach(add); textOps.forEach(add);
return this;
} }
Delta add(TextOperation textOp) { void add(TextOperation textOp) {
if (textOp.isEmpty) { if (textOp.isEmpty) {
return this; return;
} }
_rawString = null;
if (operations.isNotEmpty) { if (_operations.isNotEmpty) {
final lastOp = operations.last; final lastOp = _operations.last;
if (lastOp is TextDelete && textOp is TextDelete) { if (lastOp is TextDelete && textOp is TextDelete) {
lastOp.length += textOp.length; lastOp.length += textOp.length;
return this; return;
} }
if (mapEquals(lastOp.attributes, textOp.attributes)) { if (mapEquals(lastOp.attributes, textOp.attributes)) {
if (lastOp is TextInsert && textOp is TextInsert) { if (lastOp is TextInsert && textOp is TextInsert) {
lastOp.content += textOp.content; lastOp.content += textOp.content;
return this; return;
} }
// if there is an delete before the insert // if there is an delete before the insert
// swap the order // swap the order
if (lastOp is TextDelete && textOp is TextInsert) { if (lastOp is TextDelete && textOp is TextInsert) {
operations.removeLast(); _operations.removeLast();
operations.add(textOp); _operations.add(textOp);
operations.add(lastOp); _operations.add(lastOp);
return this; return;
} }
if (lastOp is TextRetain && textOp is TextRetain) { if (lastOp is TextRetain && textOp is TextRetain) {
lastOp.length += textOp.length; lastOp.length += textOp.length;
return this; return;
} }
} }
} }
operations.add(textOp); _operations.add(textOp);
return this;
} }
Delta slice(int start, [int? end]) { Delta slice(int start, [int? end]) {
final result = Delta(); final result = Delta();
final iterator = _OpIterator(operations); final iterator = _OpIterator(_operations);
int index = 0; int index = 0;
while ((end == null || index < end) && iterator.hasNext) { while ((end == null || index < end) && iterator.hasNext) {
@ -335,29 +336,22 @@ class Delta {
return result; return result;
} }
Delta insert(String content, [Attributes? attributes]) { void insert(String content, [Attributes? attributes]) =>
final op = TextInsert(content, attributes); add(TextInsert(content, attributes));
return add(op);
}
Delta retain(int length, [Attributes? attributes]) { void retain(int length, [Attributes? attributes]) =>
final op = TextRetain(length, attributes); add(TextRetain(length, attributes));
return add(op);
}
Delta delete(int length) { void delete(int length) => add(TextDelete(length));
final op = TextDelete(length);
return add(op);
}
int get length { int get length {
return operations.fold( return _operations.fold(
0, (previousValue, element) => previousValue + element.length); 0, (previousValue, element) => previousValue + element.length);
} }
Delta compose(Delta other) { Delta compose(Delta other) {
final thisIter = _OpIterator(operations); final thisIter = _OpIterator(_operations);
final otherIter = _OpIterator(other.operations); final otherIter = _OpIterator(other._operations);
final ops = <TextOperation>[]; final ops = <TextOperation>[];
final firstOther = otherIter.peek(); final firstOther = otherIter.peek();
@ -405,9 +399,9 @@ class Delta {
// Optimization if rest of other is just retain // Optimization if rest of other is just retain
if (!otherIter.hasNext && if (!otherIter.hasNext &&
delta.operations[delta.operations.length - 1] == newOp) { delta._operations[delta._operations.length - 1] == newOp) {
final rest = Delta(thisIter.rest()); final rest = Delta(thisIter.rest());
return delta.concat(rest).chop(); return (delta + rest)..chop();
} }
} else if (otherOp is TextDelete && (thisOp is TextRetain)) { } else if (otherOp is TextDelete && (thisOp is TextRetain)) {
delta.add(otherOp); delta.add(otherOp);
@ -415,27 +409,27 @@ class Delta {
} }
} }
return delta.chop(); return delta..chop();
} }
Delta concat(Delta other) { Delta operator +(Delta other) {
var ops = [...operations]; var ops = [..._operations];
if (other.operations.isNotEmpty) { if (other._operations.isNotEmpty) {
ops.add(other.operations[0]); ops.add(other._operations[0]);
ops.addAll(other.operations.sublist(1)); ops.addAll(other._operations.sublist(1));
} }
return Delta(ops); return Delta(ops);
} }
Delta chop() { void chop() {
if (operations.isEmpty) { if (_operations.isEmpty) {
return this; return;
} }
final lastOp = operations.last; _rawString = null;
final lastOp = _operations.last;
if (lastOp is TextRetain && (lastOp.attributes?.length ?? 0) == 0) { if (lastOp is TextRetain && (lastOp.attributes?.length ?? 0) == 0) {
operations.removeLast(); _operations.removeLast();
} }
return this;
} }
@override @override
@ -443,17 +437,17 @@ class Delta {
if (other is! Delta) { if (other is! Delta) {
return false; return false;
} }
return listEquals(operations, other.operations); return listEquals(_operations, other._operations);
} }
@override @override
int get hashCode { int get hashCode {
return hashList(operations); return hashList(_operations);
} }
Delta invert(Delta base) { Delta invert(Delta base) {
final inverted = Delta(); final inverted = Delta();
operations.fold(0, (int previousValue, op) { _operations.fold(0, (int previousValue, op) {
if (op is TextInsert) { if (op is TextInsert) {
inverted.delete(op.length); inverted.delete(op.length);
} else if (op is TextRetain && op.attributes == null) { } else if (op is TextRetain && op.attributes == null) {
@ -462,7 +456,7 @@ class Delta {
} else if (op is TextDelete || op is TextRetain) { } else if (op is TextDelete || op is TextRetain) {
final length = op.length; final length = op.length;
final slice = base.slice(previousValue, previousValue + length); final slice = base.slice(previousValue, previousValue + length);
for (final baseOp in slice.operations) { for (final baseOp in slice._operations) {
if (op is TextDelete) { if (op is TextDelete) {
inverted.add(baseOp); inverted.add(baseOp);
} else if (op is TextRetain && op.attributes != null) { } else if (op is TextRetain && op.attributes != null) {
@ -474,10 +468,58 @@ class Delta {
} }
return previousValue; return previousValue;
}); });
return inverted.chop(); return inverted..chop();
} }
List<dynamic> toJson() { List<dynamic> toJson() {
return operations.map((e) => e.toJson()).toList(); return _operations.map((e) => e.toJson()).toList();
} }
int prevRunePosition(int pos) {
if (pos == 0) {
return pos - 1;
}
_rawString ??=
_operations.whereType<TextInsert>().map((op) => op.content).join();
_runeIndexes ??= stringIndexes(_rawString!);
return _runeIndexes![pos - 1];
}
int nextRunePosition(int pos) {
final stringContent = toRawString();
if (pos >= stringContent.length - 1) {
return stringContent.length;
}
_runeIndexes ??= stringIndexes(_rawString!);
for (var i = pos + 1; i < _runeIndexes!.length; i++) {
if (_runeIndexes![i] != pos) {
return _runeIndexes![i];
}
}
return stringContent.length;
}
String toRawString() {
_rawString ??=
_operations.whereType<TextInsert>().map((op) => op.content).join();
return _rawString!;
}
@override
Iterator<TextOperation> get iterator => _operations.iterator;
}
List<int> stringIndexes(String content) {
final indexes = List<int>.filled(content.length, 0);
final iterator = content.runes.iterator;
while (iterator.moveNext()) {
for (var i = 0; i < iterator.currentSize; i++) {
indexes[iterator.rawIndex + i] = iterator.rawIndex;
}
}
return indexes;
} }

View File

@ -19,7 +19,7 @@ extension TextNodeExtension on TextNode {
allSatisfyInSelection(StyleKey.strikethrough, selection); allSatisfyInSelection(StyleKey.strikethrough, selection);
bool allSatisfyInSelection(String styleKey, Selection selection) { bool allSatisfyInSelection(String styleKey, Selection selection) {
final ops = delta.operations.whereType<TextInsert>(); final ops = delta.whereType<TextInsert>();
var start = 0; var start = 0;
for (final op in ops) { for (final op in ops) {
if (start >= selection.end.offset) { if (start >= selection.end.offset) {

View File

@ -71,7 +71,7 @@ class HTMLToNodesConverter {
delta.insert(child.text ?? ""); delta.insert(child.text ?? "");
} }
} }
if (delta.operations.isNotEmpty) { if (delta.isNotEmpty) {
result.add(TextNode(type: "text", delta: delta)); result.add(TextNode(type: "text", delta: delta));
} }
return result; return result;
@ -101,7 +101,7 @@ class HTMLToNodesConverter {
} else { } else {
final delta = Delta(); final delta = Delta();
delta.insert(element.text); delta.insert(element.text);
if (delta.operations.isNotEmpty) { if (delta.isNotEmpty) {
return [TextNode(type: "text", delta: delta)]; return [TextNode(type: "text", delta: delta)];
} }
} }
@ -446,7 +446,7 @@ class NodesToHTMLConverter {
childNodes.add(node); childNodes.add(node);
} }
for (final op in delta.operations) { for (final op in delta) {
if (op is TextInsert) { if (op is TextInsert) {
final attributes = op.attributes; final attributes = op.attributes;
if (attributes != null) { if (attributes != null) {

View File

@ -94,7 +94,7 @@ class TransactionBuilder {
() => Delta() () => Delta()
..retain(firstOffset ?? firstLength) ..retain(firstOffset ?? firstLength)
..delete(firstLength - (firstOffset ?? firstLength)) ..delete(firstLength - (firstOffset ?? firstLength))
..addAll(secondNode.delta.slice(secondOffset, secondLength).operations), ..addAll(secondNode.delta.slice(secondOffset, secondLength)),
); );
afterSelection = Selection.collapsed( afterSelection = Selection.collapsed(
Position( Position(
@ -108,30 +108,37 @@ class TransactionBuilder {
[Attributes? attributes]) { [Attributes? attributes]) {
var newAttributes = attributes; var newAttributes = attributes;
if (index != 0 && attributes == null) { if (index != 0 && attributes == null) {
newAttributes = node.delta newAttributes =
.slice(max(index - 1, 0), index) node.delta.slice(max(index - 1, 0), index).first.attributes;
.operations
.first
.attributes;
} }
textEdit( textEdit(
node, node,
() => Delta().retain(index).insert( () => Delta()
content, ..retain(index)
newAttributes, ..insert(
), content,
newAttributes,
),
); );
afterSelection = Selection.collapsed( afterSelection = Selection.collapsed(
Position(path: node.path, offset: index + content.length)); Position(path: node.path, offset: index + content.length));
} }
formatText(TextNode node, int index, int length, Attributes attributes) { formatText(TextNode node, int index, int length, Attributes attributes) {
textEdit(node, () => Delta().retain(index).retain(length, attributes)); textEdit(
node,
() => Delta()
..retain(index)
..retain(length, attributes));
afterSelection = beforeSelection; afterSelection = beforeSelection;
} }
deleteText(TextNode node, int index, int length) { deleteText(TextNode node, int index, int length) {
textEdit(node, () => Delta().retain(index).delete(length)); textEdit(
node,
() => Delta()
..retain(index)
..delete(length));
afterSelection = afterSelection =
Selection.collapsed(Position(path: node.path, offset: index)); Selection.collapsed(Position(path: node.path, offset: index));
} }
@ -140,14 +147,17 @@ class TransactionBuilder {
[Attributes? attributes]) { [Attributes? attributes]) {
var newAttributes = attributes; var newAttributes = attributes;
if (attributes == null) { if (attributes == null) {
final ops = node.delta.slice(index, index + length).operations; final ops = node.delta.slice(index, index + length);
if (ops.isNotEmpty) { if (ops.isNotEmpty) {
newAttributes = ops.first.attributes; newAttributes = ops.first.attributes;
} }
} }
textEdit( textEdit(
node, node,
() => Delta().retain(index).delete(length).insert(content, newAttributes), () => Delta()
..retain(index)
..delete(length)
..insert(content, newAttributes),
); );
afterSelection = Selection.collapsed( afterSelection = Selection.collapsed(
Position( Position(

View File

@ -198,7 +198,7 @@ class _FlowyRichTextState extends State<FlowyRichText> with Selectable {
} }
TextSpan get _textSpan => TextSpan( TextSpan get _textSpan => TextSpan(
children: widget.textNode.delta.operations children: widget.textNode.delta
.whereType<TextInsert>() .whereType<TextInsert>()
.map((insert) => RichTextStyle( .map((insert) => RichTextStyle(
attributes: insert.attributes ?? {}, attributes: insert.attributes ?? {},

View File

@ -12,8 +12,8 @@ int _endOffsetOfNode(Node node) {
extension on Position { extension on Position {
Position? goLeft(EditorState editorState) { Position? goLeft(EditorState editorState) {
final node = editorState.document.nodeAtPath(path)!;
if (offset == 0) { if (offset == 0) {
final node = editorState.document.nodeAtPath(path)!;
final prevNode = node.previous; final prevNode = node.previous;
if (prevNode != null) { if (prevNode != null) {
return Position( return Position(
@ -22,7 +22,11 @@ extension on Position {
return null; return null;
} }
return Position(path: path, offset: offset - 1); if (node is TextNode) {
return Position(path: path, offset: node.delta.prevRunePosition(offset));
} else {
return Position(path: path, offset: offset);
}
} }
Position? goRight(EditorState editorState) { Position? goRight(EditorState editorState) {
@ -36,7 +40,11 @@ extension on Position {
return null; return null;
} }
return Position(path: path, offset: offset + 1); if (node is TextNode) {
return Position(path: path, offset: node.delta.nextRunePosition(offset));
} else {
return Position(path: path, offset: offset);
}
} }
} }

View File

@ -67,7 +67,7 @@ _pasteHTML(EditorState editorState, String html) {
final textNodeAtPath = nodeAtPath as TextNode; final textNodeAtPath = nodeAtPath as TextNode;
final firstTextNode = firstNode as TextNode; final firstTextNode = firstNode as TextNode;
tb.textEdit(textNodeAtPath, tb.textEdit(textNodeAtPath,
() => Delta().retain(startOffset).concat(firstTextNode.delta)); () => (Delta()..retain(startOffset)) + firstTextNode.delta);
tb.setAfterSelection(Selection.collapsed(Position( tb.setAfterSelection(Selection.collapsed(Position(
path: path, offset: startOffset + firstTextNode.delta.length))); path: path, offset: startOffset + firstTextNode.delta.length)));
tb.commit(); tb.commit();
@ -93,17 +93,18 @@ _pasteMultipleLinesInText(
tb.textEdit( tb.textEdit(
textNodeAtPath, textNodeAtPath,
() => Delta() () =>
.retain(offset) (Delta()
.delete(remain.length) ..retain(offset)
.concat(firstTextNode.delta)); ..delete(remain.length)) +
firstTextNode.delta);
final tailNodes = nodes.sublist(1); final tailNodes = nodes.sublist(1);
path[path.length - 1]++; path[path.length - 1]++;
if (tailNodes.isNotEmpty) { if (tailNodes.isNotEmpty) {
if (tailNodes.last.type == "text") { if (tailNodes.last.type == "text") {
final tailTextNode = tailNodes.last as TextNode; final tailTextNode = tailNodes.last as TextNode;
tailTextNode.delta = tailTextNode.delta.concat(remain); tailTextNode.delta = tailTextNode.delta + remain;
} else if (remain.length > 0) { } else if (remain.length > 0) {
tailNodes.add(TextNode(type: "text", delta: remain)); tailNodes.add(TextNode(type: "text", delta: remain));
} }
@ -151,7 +152,11 @@ _handlePastePlainText(EditorState editorState, String plainText) {
editorState.document.nodeAtPath(selection.end.path)! as TextNode; editorState.document.nodeAtPath(selection.end.path)! as TextNode;
final beginOffset = selection.end.offset; final beginOffset = selection.end.offset;
TransactionBuilder(editorState) TransactionBuilder(editorState)
..textEdit(node, () => Delta().retain(beginOffset).insert(lines[0])) ..textEdit(
node,
() => Delta()
..retain(beginOffset)
..insert(lines[0]))
..setAfterSelection(Selection.collapsed(Position( ..setAfterSelection(Selection.collapsed(Position(
path: selection.end.path, offset: beginOffset + lines[0].length))) path: selection.end.path, offset: beginOffset + lines[0].length)))
..commit(); ..commit();
@ -176,17 +181,19 @@ _handlePastePlainText(EditorState editorState, String plainText) {
if (index++ == remains.length - 1) { if (index++ == remains.length - 1) {
return TextNode( return TextNode(
type: "text", type: "text",
delta: Delta().insert(e).addAll(insertedLineSuffix.operations)); delta: Delta()
..insert(e)
..addAll(insertedLineSuffix));
} }
return TextNode(type: "text", delta: Delta().insert(e)); return TextNode(type: "text", delta: Delta()..insert(e));
}).toList(); }).toList();
// insert first line // insert first line
tb.textEdit( tb.textEdit(
node, node,
() => Delta() () => Delta()
.retain(beginOffset) ..retain(beginOffset)
.insert(firstLine) ..insert(firstLine)
.delete(node.delta.length - beginOffset)); ..delete(node.delta.length - beginOffset));
// insert remains // insert remains
tb.insertNodes(path, nodes); tb.insertNodes(path, nodes);
tb.commit(); tb.commit();
@ -227,7 +234,10 @@ _deleteSelectedContent(EditorState editorState) {
final tb = TransactionBuilder(editorState); final tb = TransactionBuilder(editorState);
final len = selection.end.offset - selection.start.offset; final len = selection.end.offset - selection.start.offset;
tb.textEdit( tb.textEdit(
textItem, () => Delta().retain(selection.start.offset).delete(len)); textItem,
() => Delta()
..retain(selection.start.offset)
..delete(len));
tb.setAfterSelection(Selection.collapsed(selection.start)); tb.setAfterSelection(Selection.collapsed(selection.start));
tb.commit(); tb.commit();
return; return;
@ -241,12 +251,13 @@ _deleteSelectedContent(EditorState editorState) {
final textItem = item as TextNode; final textItem = item as TextNode;
final deleteLen = textItem.delta.length - selection.start.offset; final deleteLen = textItem.delta.length - selection.start.offset;
tb.textEdit(textItem, () { tb.textEdit(textItem, () {
final delta = Delta(); final delta = Delta()
delta.retain(selection.start.offset).delete(deleteLen); ..retain(selection.start.offset)
..delete(deleteLen);
if (endNode is TextNode) { if (endNode is TextNode) {
final remain = endNode.delta.slice(selection.end.offset); final remain = endNode.delta.slice(selection.end.offset);
delta.addAll(remain.operations); delta.addAll(remain);
} }
return delta; return delta;

View File

@ -25,7 +25,7 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) {
TransactionBuilder transactionBuilder = TransactionBuilder(editorState); TransactionBuilder transactionBuilder = TransactionBuilder(editorState);
if (textNodes.length == 1) { if (textNodes.length == 1) {
final textNode = textNodes.first; final textNode = textNodes.first;
final index = selection.start.offset - 1; final index = textNode.delta.prevRunePosition(selection.start.offset);
if (index < 0) { if (index < 0) {
// 1. style // 1. style
if (textNode.subtype != null) { if (textNode.subtype != null) {
@ -62,8 +62,8 @@ FlowyKeyEventHandler deleteTextHandler = (editorState, event) {
if (selection.isCollapsed) { if (selection.isCollapsed) {
transactionBuilder.deleteText( transactionBuilder.deleteText(
textNode, textNode,
selection.start.offset - 1, index,
1, selection.start.offset - index,
); );
} else { } else {
transactionBuilder.deleteText( transactionBuilder.deleteText(

View File

@ -14,176 +14,218 @@ void main() {
}) })
]); ]);
final death = Delta().retain(12).insert("White", { final death = Delta()
'color': '#fff', ..retain(12)
}).delete(4); ..insert("White", {
'color': '#fff',
})
..delete(4);
final restores = delta.compose(death); final restores = delta.compose(death);
expect(restores.operations, <TextOperation>[ expect(restores.toList(), <TextOperation>[
TextInsert('Gandalf', {'bold': true}), TextInsert('Gandalf', {'bold': true}),
TextInsert(' the '), TextInsert(' the '),
TextInsert('White', {'color': '#fff'}), TextInsert('White', {'color': '#fff'}),
]); ]);
}); });
test('compose()', () { test('compose()', () {
final a = Delta().insert('A'); final a = Delta()..insert('A');
final b = Delta().insert('B'); final b = Delta()..insert('B');
final expected = Delta().insert('B').insert('A'); final expected = Delta()
..insert('B')
..insert('A');
expect(a.compose(b), expected); expect(a.compose(b), expected);
}); });
test('insert + retain', () { test('insert + retain', () {
final a = Delta().insert('A'); final a = Delta()..insert('A');
final b = Delta().retain(1, { final b = Delta()
'bold': true, ..retain(1, {
'color': 'red', 'bold': true,
}); 'color': 'red',
final expected = Delta().insert('A', { });
'bold': true, final expected = Delta()
'color': 'red', ..insert('A', {
}); 'bold': true,
'color': 'red',
});
expect(a.compose(b), expected); expect(a.compose(b), expected);
}); });
test('insert + delete', () { test('insert + delete', () {
final a = Delta().insert('A'); final a = Delta()..insert('A');
final b = Delta().delete(1); final b = Delta()..delete(1);
final expected = Delta(); final expected = Delta();
expect(a.compose(b), expected); expect(a.compose(b), expected);
}); });
test('delete + insert', () { test('delete + insert', () {
final a = Delta().delete(1); final a = Delta()..delete(1);
final b = Delta().insert('B'); final b = Delta()..insert('B');
final expected = Delta().insert('B').delete(1); final expected = Delta()
..insert('B')
..delete(1);
expect(a.compose(b), expected); expect(a.compose(b), expected);
}); });
test('delete + retain', () { test('delete + retain', () {
final a = Delta().delete(1); final a = Delta()..delete(1);
final b = Delta().retain(1, { final b = Delta()
'bold': true, ..retain(1, {
'color': 'red', 'bold': true,
}); 'color': 'red',
final expected = Delta().delete(1).retain(1, { });
'bold': true, final expected = Delta()
'color': 'red', ..delete(1)
}); ..retain(1, {
'bold': true,
'color': 'red',
});
expect(a.compose(b), expected); expect(a.compose(b), expected);
}); });
test('delete + delete', () { test('delete + delete', () {
final a = Delta().delete(1); final a = Delta()..delete(1);
final b = Delta().delete(1); final b = Delta()..delete(1);
final expected = Delta().delete(2); final expected = Delta()..delete(2);
expect(a.compose(b), expected); expect(a.compose(b), expected);
}); });
test('retain + insert', () { test('retain + insert', () {
final a = Delta().retain(1, {'color': 'blue'}); final a = Delta()..retain(1, {'color': 'blue'});
final b = Delta().insert('B'); final b = Delta()..insert('B');
final expected = Delta().insert('B').retain(1, { final expected = Delta()
'color': 'blue', ..insert('B')
}); ..retain(1, {
'color': 'blue',
});
expect(a.compose(b), expected); expect(a.compose(b), expected);
}); });
test('retain + retain', () { test('retain + retain', () {
final a = Delta().retain(1, { final a = Delta()
'color': 'blue', ..retain(1, {
}); 'color': 'blue',
final b = Delta().retain(1, { });
'bold': true, final b = Delta()
'color': 'red', ..retain(1, {
}); 'bold': true,
final expected = Delta().retain(1, { 'color': 'red',
'bold': true, });
'color': 'red', final expected = Delta()
}); ..retain(1, {
'bold': true,
'color': 'red',
});
expect(a.compose(b), expected); expect(a.compose(b), expected);
}); });
test('retain + delete', () { test('retain + delete', () {
final a = Delta().retain(1, { final a = Delta()
'color': 'blue', ..retain(1, {
}); 'color': 'blue',
final b = Delta().delete(1); });
final expected = Delta().delete(1); final b = Delta()..delete(1);
final expected = Delta()..delete(1);
expect(a.compose(b), expected); expect(a.compose(b), expected);
}); });
test('insert in middle of text', () { test('insert in middle of text', () {
final a = Delta().insert('Hello'); final a = Delta()..insert('Hello');
final b = Delta().retain(3).insert('X'); final b = Delta()
final expected = Delta().insert('HelXlo'); ..retain(3)
..insert('X');
final expected = Delta()..insert('HelXlo');
expect(a.compose(b), expected); expect(a.compose(b), expected);
}); });
test('insert and delete ordering', () { test('insert and delete ordering', () {
final a = Delta().insert('Hello'); final a = Delta()..insert('Hello');
final b = Delta().insert('Hello'); final b = Delta()..insert('Hello');
final insertFirst = Delta().retain(3).insert('X').delete(1); final insertFirst = Delta()
final deleteFirst = Delta().retain(3).delete(1).insert('X'); ..retain(3)
final expected = Delta().insert('HelXo'); ..insert('X')
..delete(1);
final deleteFirst = Delta()
..retain(3)
..delete(1)
..insert('X');
final expected = Delta()..insert('HelXo');
expect(a.compose(insertFirst), expected); expect(a.compose(insertFirst), expected);
expect(b.compose(deleteFirst), expected); expect(b.compose(deleteFirst), expected);
}); });
test('delete entire text', () { test('delete entire text', () {
final a = Delta().retain(4).insert('Hello'); final a = Delta()
final b = Delta().delete(9); ..retain(4)
final expected = Delta().delete(4); ..insert('Hello');
final b = Delta()..delete(9);
final expected = Delta()..delete(4);
expect(a.compose(b), expected); expect(a.compose(b), expected);
}); });
test('retain more than length of text', () { test('retain more than length of text', () {
final a = Delta().insert('Hello'); final a = Delta()..insert('Hello');
final b = Delta().retain(10); final b = Delta()..retain(10);
final expected = Delta().insert('Hello'); final expected = Delta()..insert('Hello');
expect(a.compose(b), expected); expect(a.compose(b), expected);
}); });
test('retain start optimization', () { test('retain start optimization', () {
final a = Delta() final a = Delta()
.insert('A', {'bold': true}) ..insert('A', {'bold': true})
.insert('B') ..insert('B')
.insert('C', {'bold': true}) ..insert('C', {'bold': true})
.delete(1); ..delete(1);
final b = Delta().retain(3).insert('D'); final b = Delta()
..retain(3)
..insert('D');
final expected = Delta() final expected = Delta()
.insert('A', {'bold': true}) ..insert('A', {'bold': true})
.insert('B') ..insert('B')
.insert('C', {'bold': true}) ..insert('C', {'bold': true})
.insert('D') ..insert('D')
.delete(1); ..delete(1);
expect(a.compose(b), expected); expect(a.compose(b), expected);
}); });
test('retain end optimization', () { test('retain end optimization', () {
final a = Delta() final a = Delta()
.insert('A', {'bold': true}) ..insert('A', {'bold': true})
.insert('B') ..insert('B')
.insert('C', {'bold': true}); ..insert('C', {'bold': true});
final b = Delta().delete(1); final b = Delta()..delete(1);
final expected = Delta().insert('B').insert('C', {'bold': true}); final expected = Delta()
..insert('B')
..insert('C', {'bold': true});
expect(a.compose(b), expected); expect(a.compose(b), expected);
}); });
test('retain end optimization join', () { test('retain end optimization join', () {
final a = Delta() final a = Delta()
.insert('A', {'bold': true}) ..insert('A', {'bold': true})
.insert('B') ..insert('B')
.insert('C', {'bold': true}) ..insert('C', {'bold': true})
.insert('D') ..insert('D')
.insert('E', {'bold': true}) ..insert('E', {'bold': true})
.insert('F'); ..insert('F');
final b = Delta().retain(1).delete(1); final b = Delta()
..retain(1)
..delete(1);
final expected = Delta() final expected = Delta()
.insert('AC', {'bold': true}) ..insert('AC', {'bold': true})
.insert('D') ..insert('D')
.insert('E', {'bold': true}) ..insert('E', {'bold': true})
.insert('F'); ..insert('F');
expect(a.compose(b), expected); expect(a.compose(b), expected);
}); });
}); });
group('invert', () { group('invert', () {
test('insert', () { test('insert', () {
final delta = Delta().retain(2).insert('A'); final delta = Delta()
final base = Delta().insert('12346'); ..retain(2)
final expected = Delta().retain(2).delete(1); ..insert('A');
final base = Delta()..insert('12346');
final expected = Delta()
..retain(2)
..delete(1);
final inverted = delta.invert(base); final inverted = delta.invert(base);
expect(expected, inverted); expect(expected, inverted);
expect(base.compose(delta).compose(inverted), base); expect(base.compose(delta).compose(inverted), base);
}); });
test('delete', () { test('delete', () {
final delta = Delta().retain(2).delete(3); final delta = Delta()
final base = Delta().insert('123456'); ..retain(2)
final expected = Delta().retain(2).insert('345'); ..delete(3);
final base = Delta()..insert('123456');
final expected = Delta()
..retain(2)
..insert('345');
final inverted = delta.invert(base); final inverted = delta.invert(base);
expect(expected, inverted); expect(expected, inverted);
expect(base.compose(delta).compose(inverted), base); expect(base.compose(delta).compose(inverted), base);
@ -199,7 +241,10 @@ void main() {
}); });
group('json', () { group('json', () {
test('toJson()', () { test('toJson()', () {
final delta = Delta().retain(2).insert('A').delete(3); final delta = Delta()
..retain(2)
..insert('A')
..delete(3);
expect(delta.toJson(), [ expect(delta.toJson(), [
{'retain': 2}, {'retain': 2},
{'insert': 'A'}, {'insert': 'A'},
@ -207,8 +252,9 @@ void main() {
]); ]);
}); });
test('attributes', () { test('attributes', () {
final delta = final delta = Delta()
Delta().retain(2, {'bold': true}).insert('A', {'italic': true}); ..retain(2, {'bold': true})
..insert('A', {'italic': true});
expect(delta.toJson(), [ expect(delta.toJson(), [
{ {
'retain': 2, 'retain': 2,
@ -226,8 +272,42 @@ void main() {
{'insert': 'A'}, {'insert': 'A'},
{'delete': 3}, {'delete': 3},
]); ]);
final expected = Delta().retain(2).insert('A').delete(3); final expected = Delta()
..retain(2)
..insert('A')
..delete(3);
expect(delta, expected); expect(delta, expected);
}); });
}); });
group('runes', () {
test("stringIndexes", () {
final indexes = stringIndexes('😊');
expect(indexes[0], 0);
expect(indexes[1], 0);
});
test("next rune 1", () {
final delta = Delta()..insert('😊');
expect(delta.nextRunePosition(0), 2);
});
test("next rune 2", () {
final delta = Delta()..insert('😊a');
expect(delta.nextRunePosition(0), 2);
});
test("next rune 3", () {
final delta = Delta()..insert('😊陈');
expect(delta.nextRunePosition(2), 3);
});
test("prev rune 1", () {
final delta = Delta()..insert('😊陈');
expect(delta.prevRunePosition(2), 0);
});
test("prev rune 2", () {
final delta = Delta()..insert('😊');
expect(delta.prevRunePosition(2), 0);
});
test("prev rune 3", () {
final delta = Delta()..insert('😊');
expect(delta.prevRunePosition(0), -1);
});
});
} }