diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart index a60f04e89b..a87064d85a 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/position.dart @@ -34,4 +34,11 @@ class Position { @override String toString() => 'path = $path, offset = $offset'; + + Map toJson() { + return { + "path": path.toList(), + "offset": offset, + }; + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart index a3919a21f6..f1fa0682f6 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/selection.dart @@ -48,4 +48,11 @@ class Selection { @override String toString() => '[Selection] start = $start, end = $end'; + + Map toJson() { + return { + "start": start.toJson(), + "end": end.toJson(), + }; + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart index eafa4a31da..a505df4b1d 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/operation.dart @@ -2,22 +2,40 @@ import 'package:flowy_editor/document/attributes.dart'; import 'package:flowy_editor/flowy_editor.dart'; abstract class Operation { + factory Operation.fromJson(Map map) { + String t = map["type"] as String; + if (t == "insert-operation") { + return InsertOperation.fromJson(map); + } else if (t == "update-operation") { + return UpdateOperation.fromJson(map); + } else if (t == "delete-operation") { + return DeleteOperation.fromJson(map); + } else if (t == "text-edit-operation") { + return TextEditOperation.fromJson(map); + } + + throw ArgumentError('unexpected type $t'); + } final Path path; - Operation({required this.path}); + Operation(this.path); Operation copyWithPath(Path path); Operation invert(); + Map toJson(); } class InsertOperation extends Operation { final Node value; - InsertOperation({ - required super.path, - required this.value, - }); + factory InsertOperation.fromJson(Map map) { + final path = map["path"] as List; + final value = Node.fromJson(map["value"]); + return InsertOperation(path, value); + } + + InsertOperation(Path path, this.value) : super(path); InsertOperation copyWith({Path? path, Node? value}) => - InsertOperation(path: path ?? this.path, value: value ?? this.value); + InsertOperation(path ?? this.path, value ?? this.value); @override Operation copyWithPath(Path path) => copyWith(path: path); @@ -25,28 +43,42 @@ class InsertOperation extends Operation { @override Operation invert() { return DeleteOperation( - path: path, - removedValue: value, + path, + value, ); } + + @override + Map toJson() { + return { + "type": "insert-operation", + "path": path.toList(), + "value": value.toJson(), + }; + } } class UpdateOperation extends Operation { final Attributes attributes; final Attributes oldAttributes; - UpdateOperation({ - required super.path, - required this.attributes, - required this.oldAttributes, - }); + factory UpdateOperation.fromJson(Map map) { + final path = map["path"] as List; + final attributes = map["attributes"] as Map; + final oldAttributes = map["oldAttributes"] as Map; + return UpdateOperation(path, attributes, oldAttributes); + } + + UpdateOperation( + Path path, + this.attributes, + this.oldAttributes, + ) : super(path); UpdateOperation copyWith( {Path? path, Attributes? attributes, Attributes? oldAttributes}) => - UpdateOperation( - path: path ?? this.path, - attributes: attributes ?? this.attributes, - oldAttributes: oldAttributes ?? this.oldAttributes); + UpdateOperation(path ?? this.path, attributes ?? this.attributes, + oldAttributes ?? this.oldAttributes); @override Operation copyWithPath(Path path) => copyWith(path: path); @@ -54,33 +86,55 @@ class UpdateOperation extends Operation { @override Operation invert() { return UpdateOperation( - path: path, - attributes: oldAttributes, - oldAttributes: attributes, + path, + oldAttributes, + attributes, ); } + + @override + Map toJson() { + return { + "type": "update-operation", + "path": path.toList(), + "attributes": {...attributes}, + "oldAttributes": {...oldAttributes}, + }; + } } class DeleteOperation extends Operation { final Node removedValue; - DeleteOperation({ - required super.path, - required this.removedValue, - }); + factory DeleteOperation.fromJson(Map map) { + final path = map["path"] as List; + final removedValue = Node.fromJson(map["removedValue"]); + return DeleteOperation(path, removedValue); + } - DeleteOperation copyWith({Path? path, Node? removedValue}) => DeleteOperation( - path: path ?? this.path, removedValue: removedValue ?? this.removedValue); + DeleteOperation( + Path path, + this.removedValue, + ) : super(path); + + DeleteOperation copyWith({Path? path, Node? removedValue}) => + DeleteOperation(path ?? this.path, removedValue ?? this.removedValue); @override Operation copyWithPath(Path path) => copyWith(path: path); @override Operation invert() { - return InsertOperation( - path: path, - value: removedValue, - ); + return InsertOperation(path, removedValue); + } + + @override + Map toJson() { + return { + "type": "delete-operation", + "path": path.toList(), + "removedValue": removedValue.toJson(), + }; } } @@ -88,24 +142,39 @@ class TextEditOperation extends Operation { final Delta delta; final Delta inverted; - TextEditOperation({ - required super.path, - required this.delta, - required this.inverted, - }); + factory TextEditOperation.fromJson(Map map) { + final path = map["path"] as List; + final delta = Delta.fromJson(map["delta"]); + final invert = Delta.fromJson(map["invert"]); + return TextEditOperation(path, delta, invert); + } + + TextEditOperation( + Path path, + this.delta, + this.inverted, + ) : super(path); TextEditOperation copyWith({Path? path, Delta? delta, Delta? inverted}) => TextEditOperation( - path: path ?? this.path, - delta: delta ?? this.delta, - inverted: inverted ?? this.inverted); + path ?? this.path, delta ?? this.delta, inverted ?? this.inverted); @override Operation copyWithPath(Path path) => copyWith(path: path); @override Operation invert() { - return TextEditOperation(path: path, delta: inverted, inverted: delta); + return TextEditOperation(path, inverted, delta); + } + + @override + Map toJson() { + return { + "type": "text-edit-operation", + "path": path.toList(), + "delta": delta.toJson(), + "invert": inverted.toJson(), + }; } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart index 85bc43f537..5dcf167628 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction.dart @@ -23,4 +23,17 @@ class Transaction { this.beforeSelection, this.afterSelection, }); + + Map toJson() { + final Map result = { + "operations": operations.map((e) => e.toJson()), + }; + if (beforeSelection != null) { + result["beforeSelection"] = beforeSelection!.toJson(); + } + if (afterSelection != null) { + result["afterSelection"] = afterSelection!.toJson(); + } + return result; + } } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart index e70dfc411a..3b88ed1627 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/operation/transaction_builder.dart @@ -1,5 +1,4 @@ import 'dart:collection'; -import 'dart:math'; import 'package:flowy_editor/editor_state.dart'; import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/path.dart'; @@ -31,21 +30,21 @@ class TransactionBuilder { insertNode(Path path, Node node) { beforeSelection = state.cursorSelection; - add(InsertOperation(path: path, value: node)); + add(InsertOperation(path, node)); } updateNode(Node node, Attributes attributes) { beforeSelection = state.cursorSelection; add(UpdateOperation( - path: node.path, - attributes: Attributes.from(node.attributes)..addAll(attributes), - oldAttributes: node.attributes, + node.path, + Attributes.from(node.attributes)..addAll(attributes), + node.attributes, )); } deleteNode(Node node) { beforeSelection = state.cursorSelection; - add(DeleteOperation(path: node.path, removedValue: node)); + add(DeleteOperation(node.path, node)); } deleteNodes(List nodes) { @@ -60,7 +59,7 @@ class TransactionBuilder { final inverted = delta.invert(node.delta); - add(TextEditOperation(path: path, delta: delta, inverted: inverted)); + add(TextEditOperation(path, delta, inverted)); } mergeText(TextNode firstNode, TextNode secondNode, @@ -119,9 +118,9 @@ class TransactionBuilder { last is TextEditOperation && pathEquals(op.path, last.path)) { final newOp = TextEditOperation( - path: op.path, - delta: last.delta.compose(op.delta), - inverted: op.inverted.compose(last.inverted), + op.path, + last.delta.compose(op.delta), + op.inverted.compose(last.inverted), ); operations[operations.length - 1] = newOp; return; diff --git a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart index b0b6cec141..c6811b86e2 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/operation_test.dart @@ -27,26 +27,18 @@ void main() { group('transform operation', () { test('insert + insert', () { final t = transformOperation( - InsertOperation(path: [ - 0, - 1 - ], value: Node(type: "node", attributes: {}, children: LinkedList())), - InsertOperation( - path: [0, 1], - value: - Node(type: "node", attributes: {}, children: LinkedList()))); + InsertOperation([0, 1], + Node(type: "node", attributes: {}, children: LinkedList())), + InsertOperation([0, 1], + Node(type: "node", attributes: {}, children: LinkedList()))); expect(t.path, [0, 2]); }); test('delete + delete', () { final t = transformOperation( - DeleteOperation( - path: [0, 1], - removedValue: - Node(type: "node", attributes: {}, children: LinkedList())), - DeleteOperation( - path: [0, 2], - removedValue: - Node(type: "node", attributes: {}, children: LinkedList()))); + DeleteOperation([0, 1], + Node(type: "node", attributes: {}, children: LinkedList())), + DeleteOperation([0, 2], + Node(type: "node", attributes: {}, children: LinkedList()))); expect(t.path, [0, 1]); }); }); @@ -78,4 +70,48 @@ void main() { expect(transaction.operations[1].path, [0]); expect(transaction.operations[2].path, [0]); }); + group("toJson", () { + test("insert", () { + final root = Node(type: "root", attributes: {}, children: LinkedList()); + final state = EditorState(document: StateTree(root: root)); + + final item1 = Node(type: "node", attributes: {}, children: LinkedList()); + final tb = TransactionBuilder(state); + tb.insertNode([0], item1); + + final transaction = tb.finish(); + expect(transaction.toJson(), { + "operations": [ + { + "type": "insert-operation", + "path": [0], + "value": item1.toJson(), + } + ], + }); + }); + test("delete", () { + final item1 = Node(type: "node", attributes: {}, children: LinkedList()); + final root = Node( + type: "root", + attributes: {}, + children: LinkedList() + ..addAll([ + item1, + ])); + final state = EditorState(document: StateTree(root: root)); + final tb = TransactionBuilder(state); + tb.deleteNode(item1); + final transaction = tb.finish(); + expect(transaction.toJson(), { + "operations": [ + { + "type": "delete-operation", + "path": [0], + "removedValue": item1.toJson(), + } + ], + }); + }); + }); }