From 47436bf6e249190d5153171f77f10466d5c34fc7 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 11 Jul 2022 18:38:08 +0800 Subject: [PATCH 1/4] feat: find node with path or index in state tree --- .../flowy_editor/lib/document/node.dart | 18 ++++++++++++++++++ .../flowy_editor/lib/document/state_tree.dart | 5 +++++ .../flowy_editor/test/flowy_editor_test.dart | 5 +++++ 3 files changed, 28 insertions(+) diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 095ef87980..deef71b7ec 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -1,5 +1,7 @@ import 'dart:collection'; +import 'package:flowy_editor/document/path.dart'; + class Node extends LinkedListEntry { Node? parent; final String type; @@ -40,6 +42,22 @@ class Node extends LinkedListEntry { ); } + Node? childAtIndex(int index) { + if (children.length <= index) { + return null; + } + + return children.elementAt(index); + } + + Node? childAtPath(Path path) { + if (path.isEmpty) { + return this; + } + + return childAtIndex(path.first)?.childAtPath(path.sublist(1)); + } + Map toJson() { var map = { 'type': type, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart index 3c347753c4..82ee25976c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart @@ -12,4 +12,9 @@ class StateTree { final root = Node.fromJson(document); return StateTree(root: root); } + + // bool insert(Path path, Node node) { + // final insertedNode = root + // return false; + // } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart index b1b6dddb39..0c9dcfee0c 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart @@ -14,5 +14,10 @@ void main() { expect(stateTree.root.type, 'root'); expect(stateTree.root.toJson(), data['document']); expect(stateTree.root.children.last.type, 'video'); + + final checkBoxNode = stateTree.root.childAtPath([1, 0]); + expect(checkBoxNode != null, true); + final textType = checkBoxNode!.attributes['text-type']; + expect(textType != null, true); }); } From 59d92a8ced7bcd7a6346e00d32736de35f8030f3 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 11 Jul 2022 20:37:12 +0800 Subject: [PATCH 2/4] feat: insert / delelte / update / search node in state tree --- .../flowy_editor/assets/document.json | 1 + .../flowy_editor/lib/document/node.dart | 33 ++++++++++++- .../flowy_editor/lib/document/state_tree.dart | 46 +++++++++++++++++-- .../flowy_editor/test/flowy_editor_test.dart | 40 +++++++++++++++- 4 files changed, 112 insertions(+), 8 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/assets/document.json b/frontend/app_flowy/packages/flowy_editor/assets/document.json index 715b6c4e64..5c9ca5175b 100644 --- a/frontend/app_flowy/packages/flowy_editor/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/assets/document.json @@ -40,6 +40,7 @@ "attributes": { "url": "x.mp4" } + } ] } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index deef71b7ec..743ae6fd15 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -1,5 +1,4 @@ import 'dart:collection'; - import 'package:flowy_editor/document/path.dart'; class Node extends LinkedListEntry { @@ -35,11 +34,23 @@ class Node extends LinkedListEntry { ); } - return Node( + final node = Node( type: jType, children: children, attributes: jAttributes, ); + + for (final child in children) { + child.parent = node; + } + + return node; + } + + void updateAttributes(Map attributes) { + for (final attribute in attributes.entries) { + this.attributes[attribute.key] = attribute.value; + } } Node? childAtIndex(int index) { @@ -58,6 +69,24 @@ class Node extends LinkedListEntry { return childAtIndex(path.first)?.childAtPath(path.sublist(1)); } + @override + void insertAfter(Node entry) { + entry.parent = parent; + super.insertAfter(entry); + } + + @override + void insertBefore(Node entry) { + entry.parent = parent; + super.insertBefore(entry); + } + + @override + void unlink() { + parent = null; + super.unlink(); + } + Map toJson() { var map = { 'type': type, diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart index 82ee25976c..57b601c0e2 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart @@ -1,7 +1,8 @@ import 'package:flowy_editor/document/node.dart'; +import 'package:flowy_editor/document/path.dart'; class StateTree { - Node root; + final Node root; StateTree({required this.root}); @@ -13,8 +14,43 @@ class StateTree { return StateTree(root: root); } - // bool insert(Path path, Node node) { - // final insertedNode = root - // return false; - // } + Node? nodeAtPath(Path path) { + return root.childAtPath(path); + } + + bool insert(Path path, Node node) { + if (path.isEmpty) { + return false; + } + final insertedNode = root.childAtPath( + path.sublist(0, path.length - 1) + [path.last - 1], + ); + if (insertedNode == null) { + return false; + } + insertedNode.insertAfter(node); + return true; + } + + Node? delete(Path path) { + if (path.isEmpty) { + return null; + } + final deletedNode = root.childAtPath(path); + deletedNode?.unlink(); + return deletedNode; + } + + Map? update(Path path, Map attributes) { + if (path.isEmpty) { + return null; + } + final updatedNode = root.childAtPath(path); + if (updatedNode == null) { + return null; + } + final previousAttributes = {...updatedNode.attributes}; + updatedNode.updateAttributes(attributes); + return previousAttributes; + } } diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart index 0c9dcfee0c..8e3c93b865 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:flowy_editor/document/node.dart'; import 'package:flowy_editor/document/state_tree.dart'; import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -13,11 +14,48 @@ void main() { final stateTree = StateTree.fromJson(data); expect(stateTree.root.type, 'root'); expect(stateTree.root.toJson(), data['document']); - expect(stateTree.root.children.last.type, 'video'); + }); + test('search node in state tree', () async { + final String response = await rootBundle.loadString('assets/document.json'); + final data = Map.from(json.decode(response)); + final stateTree = StateTree.fromJson(data); final checkBoxNode = stateTree.root.childAtPath([1, 0]); expect(checkBoxNode != null, true); final textType = checkBoxNode!.attributes['text-type']; expect(textType != null, true); }); + + test('insert node in state tree', () async { + final String response = await rootBundle.loadString('assets/document.json'); + final data = Map.from(json.decode(response)); + final stateTree = StateTree.fromJson(data); + final insertNode = Node.fromJson({ + 'type': 'text', + }); + bool result = stateTree.insert([1, 1], insertNode); + expect(result, true); + expect(identical(insertNode, stateTree.nodeAtPath([1, 1])), true); + }); + + test('delete node in state tree', () async { + final String response = await rootBundle.loadString('assets/document.json'); + final data = Map.from(json.decode(response)); + final stateTree = StateTree.fromJson(data); + final deletedNode = stateTree.delete([1, 0]); + expect(deletedNode != null, true); + expect(deletedNode!.attributes['text-type'], 'check-box'); + }); + + test('update node in state tree', () async { + final String response = await rootBundle.loadString('assets/document.json'); + final data = Map.from(json.decode(response)); + final stateTree = StateTree.fromJson(data); + final attributes = stateTree.update([1, 0], {'text-type': 'heading1'}); + expect(attributes != null, true); + expect(attributes!['text-type'], 'check-box'); + final updatedNode = stateTree.nodeAtPath([1, 0]); + expect(updatedNode != null, true); + expect(updatedNode!.attributes['text-type'], 'heading1'); + }); } From d2e62f882b8bfb51f329c796b0d2a089791186fd Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 11 Jul 2022 21:05:43 +0800 Subject: [PATCH 3/4] feat: typedef Map to Attributes --- .../packages/flowy_editor/assets/document.json | 1 - .../packages/flowy_editor/lib/document/node.dart | 14 ++++++++------ .../flowy_editor/lib/document/state_tree.dart | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/assets/document.json b/frontend/app_flowy/packages/flowy_editor/assets/document.json index 5c9ca5175b..715b6c4e64 100644 --- a/frontend/app_flowy/packages/flowy_editor/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/assets/document.json @@ -40,7 +40,6 @@ "attributes": { "url": "x.mp4" } - } ] } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 743ae6fd15..e4ff84b99c 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart @@ -1,11 +1,13 @@ import 'dart:collection'; import 'package:flowy_editor/document/path.dart'; +typedef Attributes = Map; + class Node extends LinkedListEntry { Node? parent; final String type; final LinkedList children; - final Map attributes; + final Attributes attributes; Node({ required this.type, @@ -20,15 +22,15 @@ class Node extends LinkedListEntry { final jType = json['type'] as String; final jChildren = json['children'] as List?; final jAttributes = json['attributes'] != null - ? Map.from(json['attributes'] as Map) - : {}; + ? Attributes.from(json['attributes'] as Map) + : Attributes.from({}); final LinkedList children = LinkedList(); if (jChildren != null) { children.addAll( jChildren.map( - (jnode) => Node.fromJson( - Map.from(jnode), + (jChild) => Node.fromJson( + Map.from(jChild), ), ), ); @@ -47,7 +49,7 @@ class Node extends LinkedListEntry { return node; } - void updateAttributes(Map attributes) { + void updateAttributes(Attributes attributes) { for (final attribute in attributes.entries) { this.attributes[attribute.key] = attribute.value; } diff --git a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart index 57b601c0e2..368b575c90 100644 --- a/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart +++ b/frontend/app_flowy/packages/flowy_editor/lib/document/state_tree.dart @@ -6,7 +6,7 @@ class StateTree { StateTree({required this.root}); - factory StateTree.fromJson(Map json) { + factory StateTree.fromJson(Attributes json) { assert(json['document'] is Map); final document = Map.from(json['document'] as Map); @@ -41,7 +41,7 @@ class StateTree { return deletedNode; } - Map? update(Path path, Map attributes) { + Attributes? update(Path path, Attributes attributes) { if (path.isEmpty) { return null; } From 9e4227d3d2b36f6f072c2002b7c9d991b8911de9 Mon Sep 17 00:00:00 2001 From: "Lucas.Xu" Date: Mon, 11 Jul 2022 21:14:32 +0800 Subject: [PATCH 4/4] test: add delete node test --- .../app_flowy/packages/flowy_editor/assets/document.json | 7 +++++++ .../packages/flowy_editor/test/flowy_editor_test.dart | 9 ++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/frontend/app_flowy/packages/flowy_editor/assets/document.json b/frontend/app_flowy/packages/flowy_editor/assets/document.json index 715b6c4e64..8aa75717ac 100644 --- a/frontend/app_flowy/packages/flowy_editor/assets/document.json +++ b/frontend/app_flowy/packages/flowy_editor/assets/document.json @@ -14,6 +14,13 @@ "tag": "*" }, "children": [ + { + "type": "text", + "attributes": { + "text-type": "heading2", + "check": true + } + }, { "type": "text", "attributes": { diff --git a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart index 8e3c93b865..e8f14bc9c7 100644 --- a/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart +++ b/frontend/app_flowy/packages/flowy_editor/test/flowy_editor_test.dart @@ -42,19 +42,22 @@ void main() { final String response = await rootBundle.loadString('assets/document.json'); final data = Map.from(json.decode(response)); final stateTree = StateTree.fromJson(data); - final deletedNode = stateTree.delete([1, 0]); + final deletedNode = stateTree.delete([1, 1]); expect(deletedNode != null, true); expect(deletedNode!.attributes['text-type'], 'check-box'); + final node = stateTree.nodeAtPath([1, 1]); + expect(node != null, true); + expect(node!.attributes['tag'], '**'); }); test('update node in state tree', () async { final String response = await rootBundle.loadString('assets/document.json'); final data = Map.from(json.decode(response)); final stateTree = StateTree.fromJson(data); - final attributes = stateTree.update([1, 0], {'text-type': 'heading1'}); + final attributes = stateTree.update([1, 1], {'text-type': 'heading1'}); expect(attributes != null, true); expect(attributes!['text-type'], 'check-box'); - final updatedNode = stateTree.nodeAtPath([1, 0]); + final updatedNode = stateTree.nodeAtPath([1, 1]); expect(updatedNode != null, true); expect(updatedNode!.attributes['text-type'], 'heading1'); });