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/lib/document/node.dart b/frontend/app_flowy/packages/flowy_editor/lib/document/node.dart index 095ef87980..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,10 +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, @@ -19,25 +22,71 @@ 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), ), ), ); } - return Node( + final node = Node( type: jType, children: children, attributes: jAttributes, ); + + for (final child in children) { + child.parent = node; + } + + return node; + } + + void updateAttributes(Attributes attributes) { + for (final attribute in attributes.entries) { + this.attributes[attribute.key] = attribute.value; + } + } + + 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)); + } + + @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() { 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..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 @@ -1,15 +1,56 @@ 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}); - factory StateTree.fromJson(Map json) { + factory StateTree.fromJson(Attributes json) { assert(json['document'] is Map); final document = Map.from(json['document'] as Map); final root = Node.fromJson(document); return StateTree(root: root); } + + 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; + } + + Attributes? update(Path path, Attributes 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 b1b6dddb39..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 @@ -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,6 +14,51 @@ 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, 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, 1], {'text-type': 'heading1'}); + expect(attributes != null, true); + expect(attributes!['text-type'], 'check-box'); + final updatedNode = stateTree.nodeAtPath([1, 1]); + expect(updatedNode != null, true); + expect(updatedNode!.attributes['text-type'], 'heading1'); }); }