refactor: text-delta

This commit is contained in:
Vincent Chan 2022-08-10 16:44:37 +08:00
parent e25b507ccb
commit 737e374a54
8 changed files with 47 additions and 46 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,8 @@ 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;
factory Delta.fromJson(List<dynamic> list) { factory Delta.fromJson(List<dynamic> list) {
final operations = <TextOperation>[]; final operations = <TextOperation>[];
@ -273,9 +273,9 @@ 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) { Delta addAll(Iterable<TextOperation> textOps) {
textOps.forEach(add); textOps.forEach(add);
return this; return this;
} }
@ -285,8 +285,8 @@ class Delta {
return this; return this;
} }
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 this;
@ -299,9 +299,9 @@ class Delta {
// 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 this;
} }
if (lastOp is TextRetain && textOp is TextRetain) { if (lastOp is TextRetain && textOp is TextRetain) {
@ -311,13 +311,13 @@ class Delta {
} }
} }
operations.add(textOp); _operations.add(textOp);
return this; 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) {
@ -351,13 +351,13 @@ class Delta {
} }
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,7 +405,7 @@ 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.concat(rest).chop();
} }
@ -419,21 +419,21 @@ class Delta {
} }
Delta concat(Delta other) { Delta concat(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() { Delta chop() {
if (operations.isEmpty) { if (_operations.isEmpty) {
return this; return this;
} }
final lastOp = operations.last; 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; return this;
} }
@ -443,17 +443,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 +462,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) {
@ -478,6 +478,13 @@ class Delta {
} }
List<dynamic> toJson() { List<dynamic> toJson() {
return operations.map((e) => e.toJson()).toList(); return _operations.map((e) => e.toJson()).toList();
} }
// TODO: It's unneccesry to compute everytime.
String toRawString() =>
_operations.whereType<TextInsert>().map((op) => op.content).join();
@override
Iterator<TextOperation> get iterator => _operations.iterator;
} }

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,11 +108,8 @@ 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,
@ -140,7 +137,7 @@ 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;
} }

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

@ -175,8 +175,7 @@ _handlePastePlainText(EditorState editorState, String plainText) {
final nodes = remains.map((e) { final nodes = remains.map((e) {
if (index++ == remains.length - 1) { if (index++ == remains.length - 1) {
return TextNode( return TextNode(
type: "text", type: "text", delta: Delta().insert(e).addAll(insertedLineSuffix));
delta: Delta().insert(e).addAll(insertedLineSuffix.operations));
} }
return TextNode(type: "text", delta: Delta().insert(e)); return TextNode(type: "text", delta: Delta().insert(e));
}).toList(); }).toList();
@ -246,7 +245,7 @@ _deleteSelectedContent(EditorState editorState) {
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

@ -19,7 +19,7 @@ void main() {
}).delete(4); }).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'}),