refactor: rename state_tree to document and move document to core/state

This commit is contained in:
Lucas.Xu 2022-10-10 12:08:13 +08:00
parent d02c29426e
commit 5e7507c8e7
15 changed files with 250 additions and 153 deletions

View File

@ -98,7 +98,7 @@ class _MyHomePageState extends State<MyHomePage> {
if (snapshot.hasData && if (snapshot.hasData &&
snapshot.connectionState == ConnectionState.done) { snapshot.connectionState == ConnectionState.done) {
_editorState ??= EditorState( _editorState ??= EditorState(
document: StateTree.fromJson( document: Document.fromJson(
Map<String, Object>.from( Map<String, Object>.from(
json.decode(snapshot.data!), json.decode(snapshot.data!),
), ),

View File

@ -7,7 +7,7 @@ export 'src/core/document/node.dart';
export 'src/core/document/path.dart'; export 'src/core/document/path.dart';
export 'src/core/location/position.dart'; export 'src/core/location/position.dart';
export 'src/core/location/selection.dart'; export 'src/core/location/selection.dart';
export 'src/document/state_tree.dart'; export 'src/core/state/document.dart';
export 'src/core/document/text_delta.dart'; export 'src/core/document/text_delta.dart';
export 'src/core/document/attributes.dart'; export 'src/core/document/attributes.dart';
export 'src/document/built_in_attribute_keys.dart'; export 'src/document/built_in_attribute_keys.dart';

View File

@ -1,15 +1,15 @@
import 'package:appflowy_editor/src/core/document/node.dart'; import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/document/state_tree.dart'; import 'package:appflowy_editor/src/core/state/document.dart';
/// [NodeIterator] is used to traverse the nodes in visual order. /// [NodeIterator] is used to traverse the nodes in visual order.
class NodeIterator implements Iterator<Node> { class NodeIterator implements Iterator<Node> {
NodeIterator({ NodeIterator({
required this.stateTree, required this.document,
required this.startNode, required this.startNode,
this.endNode, this.endNode,
}); });
final StateTree stateTree; final Document document;
final Node startNode; final Node startNode;
final Node? endNode; final Node? endNode;

View File

@ -69,4 +69,22 @@ extension PathExtensions on Path {
..removeLast() ..removeLast()
..add(last + 1); ..add(last + 1);
} }
Path get previous {
Path previousPath = Path.from(this, growable: true);
if (isEmpty) {
return previousPath;
}
final last = previousPath.last;
return previousPath
..removeLast()
..add(max(0, last - 1));
}
Path get parent {
if (isEmpty) {
return this;
}
return Path.from(this, growable: true)..removeLast();
}
} }

View File

@ -0,0 +1,118 @@
import 'dart:collection';
import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/core/document/path.dart';
import 'package:appflowy_editor/src/core/document/text_delta.dart';
import '../document/attributes.dart';
/// [Document] reprensents a AppFlowy Editor document structure.
///
/// It stores the root of the document.
///
/// DO NOT directly mutate the properties of a [Document] object.
class Document {
Document({
required this.root,
});
factory Document.fromJson(Map<String, dynamic> json) {
assert(json['document'] is Map);
final document = Map<String, Object>.from(json['document'] as Map);
final root = Node.fromJson(document);
return Document(root: root);
}
/// Creates a empty document with a single text node.
factory Document.empty() {
final root = Node(
type: 'editor',
children: LinkedList<Node>()..add(TextNode.empty()),
);
return Document(
root: root,
);
}
final Node root;
/// Returns the node at the given [path].
Node? nodeAtPath(Path path) {
return root.childAtPath(path);
}
/// Inserts a [Node]s at the given [Path].
bool insert(Path path, List<Node> nodes) {
if (path.isEmpty || nodes.isEmpty) {
return false;
}
final target = nodeAtPath(path);
if (target != null) {
for (final node in nodes) {
target.insertBefore(node);
}
return true;
}
final parent = nodeAtPath(path.parent);
if (parent != null) {
for (final node in nodes) {
parent.insert(node, index: path.last);
}
return true;
}
return false;
}
/// Deletes the [Node]s at the given [Path].
bool delete(Path path, [int length = 1]) {
if (path.isEmpty || length <= 0) {
return false;
}
var target = nodeAtPath(path);
if (target == null) {
return false;
}
while (target != null && length > 0) {
final next = target.next;
target.unlink();
target = next;
length--;
}
return true;
}
/// Updates the [Node] at the given [Path]
bool update(Path path, Attributes attributes) {
if (path.isEmpty) {
return false;
}
final target = nodeAtPath(path);
if (target == null) {
return false;
}
target.updateAttributes(attributes);
return true;
}
/// Updates the [TextNode] at the given [Path]
bool updateText(Path path, Delta delta) {
if (path.isEmpty) {
return false;
}
final target = nodeAtPath(path);
if (target == null || target is! TextNode) {
return false;
}
target.delta = target.delta.compose(delta);
return true;
}
Map<String, Object> toJson() {
return {
'document': root.toJson(),
};
}
}

View File

@ -1,116 +0,0 @@
import 'dart:math';
import 'package:appflowy_editor/src/core/document/node.dart';
import 'package:appflowy_editor/src/core/document/path.dart';
import 'package:appflowy_editor/src/core/document/text_delta.dart';
import '../core/document/attributes.dart';
class StateTree {
final Node root;
StateTree({
required this.root,
});
factory StateTree.empty() {
return StateTree(
root: Node.fromJson({
'type': 'editor',
'children': [
{
'type': 'text',
}
]
}),
);
}
factory StateTree.fromJson(Attributes json) {
assert(json['document'] is Map);
final document = Map<String, Object>.from(json['document'] as Map);
final root = Node.fromJson(document);
return StateTree(root: root);
}
Map<String, Object> toJson() {
return {
'document': root.toJson(),
};
}
Node? nodeAtPath(Path path) {
return root.childAtPath(path);
}
bool insert(Path path, List<Node> nodes) {
if (path.isEmpty) {
return false;
}
Node? insertedNode = root.childAtPath(
path.sublist(0, path.length - 1) + [max(0, path.last - 1)],
);
if (insertedNode == null) {
final insertedNode = root.childAtPath(
path.sublist(0, path.length - 1),
);
if (insertedNode != null) {
for (final node in nodes) {
insertedNode.insert(node);
}
return true;
}
return false;
}
if (path.last <= 0) {
for (var i = 0; i < nodes.length; i++) {
final node = nodes[i];
insertedNode.insertBefore(node);
}
} else {
for (var i = 0; i < nodes.length; i++) {
final node = nodes[i];
insertedNode!.insertAfter(node);
insertedNode = node;
}
}
return true;
}
bool textEdit(Path path, Delta delta) {
if (path.isEmpty) {
return false;
}
final node = root.childAtPath(path);
if (node == null || node is! TextNode) {
return false;
}
node.delta = node.delta.compose(delta);
return false;
}
delete(Path path, [int length = 1]) {
if (path.isEmpty) {
return null;
}
var deletedNode = root.childAtPath(path);
while (deletedNode != null && length > 0) {
final next = deletedNode.next;
deletedNode.unlink();
length--;
deletedNode = next;
}
}
bool update(Path path, Attributes attributes) {
if (path.isEmpty) {
return false;
}
final updatedNode = root.childAtPath(path);
if (updatedNode == null) {
return false;
}
updatedNode.updateAttributes(attributes);
return true;
}
}

View File

@ -6,7 +6,7 @@ import 'package:appflowy_editor/src/service/service.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:appflowy_editor/src/core/location/selection.dart'; import 'package:appflowy_editor/src/core/location/selection.dart';
import 'package:appflowy_editor/src/document/state_tree.dart'; import 'package:appflowy_editor/src/core/state/document.dart';
import 'package:appflowy_editor/src/operation/operation.dart'; import 'package:appflowy_editor/src/operation/operation.dart';
import 'package:appflowy_editor/src/operation/transaction.dart'; import 'package:appflowy_editor/src/operation/transaction.dart';
import 'package:appflowy_editor/src/undo_manager.dart'; import 'package:appflowy_editor/src/undo_manager.dart';
@ -46,7 +46,7 @@ enum CursorUpdateReason {
/// ///
/// Mutating the document with document's API is not recommended. /// Mutating the document with document's API is not recommended.
class EditorState { class EditorState {
final StateTree document; final Document document;
// Service reference. // Service reference.
final service = FlowyService(); final service = FlowyService();
@ -105,7 +105,7 @@ class EditorState {
} }
factory EditorState.empty() { factory EditorState.empty() {
return EditorState(document: StateTree.empty()); return EditorState(document: Document.empty());
} }
/// Apply the transaction to the state. /// Apply the transaction to the state.
@ -167,7 +167,7 @@ class EditorState {
} else if (op is DeleteOperation) { } else if (op is DeleteOperation) {
document.delete(op.path, op.nodes.length); document.delete(op.path, op.nodes.length);
} else if (op is TextEditOperation) { } else if (op is TextEditOperation) {
document.textEdit(op.path, op.delta); document.updateText(op.path, op.delta);
} }
_observer.add(op); _observer.add(op);
} }

View File

@ -50,7 +50,7 @@ void _handleCopy(EditorState editorState) async {
final endNode = editorState.document.nodeAtPath(selection.end.path)!; final endNode = editorState.document.nodeAtPath(selection.end.path)!;
final nodes = NodeIterator( final nodes = NodeIterator(
stateTree: editorState.document, document: editorState.document,
startNode: beginNode, startNode: beginNode,
endNode: endNode, endNode: endNode,
).toList(); ).toList();
@ -321,7 +321,7 @@ void _deleteSelectedContent(EditorState editorState) {
return; return;
} }
final traverser = NodeIterator( final traverser = NodeIterator(
stateTree: editorState.document, document: editorState.document,
startNode: beginNode, startNode: beginNode,
endNode: endNode, endNode: endNode,
); );

View File

@ -180,7 +180,7 @@ class _AppFlowySelectionState extends State<AppFlowySelection>
final endNode = editorState.document.nodeAtPath(end); final endNode = editorState.document.nodeAtPath(end);
if (startNode != null && endNode != null) { if (startNode != null && endNode != null) {
final nodes = NodeIterator( final nodes = NodeIterator(
stateTree: editorState.document, document: editorState.document,
startNode: startNode, startNode: startNode,
endNode: endNode, endNode: endNode,
).toList(); ).toList();

View File

@ -14,7 +14,7 @@ void main() async {
root.insert(node); root.insert(node);
} }
final nodes = NodeIterator( final nodes = NodeIterator(
stateTree: StateTree(root: root), document: Document(root: root),
startNode: root.childAtPath([0])!, startNode: root.childAtPath([0])!,
endNode: root.childAtPath([10, 10]), endNode: root.childAtPath([10, 10]),
); );

View File

@ -0,0 +1,77 @@
import 'package:appflowy_editor/appflowy_editor.dart';
import 'package:flutter_test/flutter_test.dart';
void main() async {
group('documemnt.dart', () {
test('insert', () {
final document = Document.empty();
expect(document.insert([-1], []), false);
expect(document.insert([100], []), false);
final node0 = Node(type: '0');
final node1 = Node(type: '1');
expect(document.insert([0], [node0, node1]), true);
expect(document.nodeAtPath([0])?.type, '0');
expect(document.nodeAtPath([1])?.type, '1');
});
test('delete', () {
final document = Document(root: Node(type: 'root'));
expect(document.delete([-1], 1), false);
expect(document.delete([100], 1), false);
for (var i = 0; i < 10; i++) {
final node = Node(type: '$i');
document.insert([i], [node]);
}
document.delete([0], 10);
expect(document.root.children.isEmpty, true);
});
test('update', () {
final node = Node(type: 'example', attributes: {'a': 'a'});
final document = Document(root: Node(type: 'root'));
document.insert([0], [node]);
final attributes = {
'a': 'b',
'b': 'c',
};
expect(document.update([0], attributes), true);
expect(document.nodeAtPath([0])?.attributes, attributes);
expect(document.update([-1], attributes), false);
});
test('updateText', () {
final delta = Delta()..insert('Editor');
final textNode = TextNode(delta: delta);
final document = Document(root: Node(type: 'root'));
document.insert([0], [textNode]);
document.updateText([0], Delta()..insert('AppFlowy'));
expect((document.nodeAtPath([0]) as TextNode).toPlainText(),
'AppFlowyEditor');
});
test('serialize', () {
final json = {
'document': {
'type': 'editor',
'children': [
{
'type': 'text',
'delta': [],
}
],
'attributes': {'a': 'a'}
}
};
final document = Document.fromJson(json);
expect(document.toJson(), json);
});
});
}

View File

@ -19,7 +19,7 @@ class EditorWidgetTester {
EditorState get editorState => _editorState; EditorState get editorState => _editorState;
Node get root => _editorState.document.root; Node get root => _editorState.document.root;
StateTree get document => _editorState.document; Document get document => _editorState.document;
int get documentLength => _editorState.document.root.children.length; int get documentLength => _editorState.document.root.children.length;
Selection? get documentSelection => Selection? get documentSelection =>
_editorState.service.selectionService.currentSelection.value; _editorState.service.selectionService.currentSelection.value;
@ -155,7 +155,7 @@ class EditorWidgetTester {
EditorState _createEmptyDocument() { EditorState _createEmptyDocument() {
return EditorState( return EditorState(
document: StateTree( document: Document(
root: _createEmptyEditorRoot(), root: _createEmptyEditorRoot(),
), ),
)..disableSealTimer = true; )..disableSealTimer = true;

View File

@ -9,16 +9,16 @@ void main() {
test('create state tree', () async { test('create state tree', () async {
// final String response = await rootBundle.loadString('assets/document.json'); // final String response = await rootBundle.loadString('assets/document.json');
// final data = Map<String, Object>.from(json.decode(response)); // final data = Map<String, Object>.from(json.decode(response));
// final stateTree = StateTree.fromJson(data); // final document = StateTree.fromJson(data);
// expect(stateTree.root.type, 'root'); // expect(document.root.type, 'root');
// expect(stateTree.root.toJson(), data['document']); // expect(document.root.toJson(), data['document']);
}); });
test('search node by Path in state tree', () async { test('search node by Path in state tree', () async {
// final String response = await rootBundle.loadString('assets/document.json'); // final String response = await rootBundle.loadString('assets/document.json');
// final data = Map<String, Object>.from(json.decode(response)); // final data = Map<String, Object>.from(json.decode(response));
// final stateTree = StateTree.fromJson(data); // final document = StateTree.fromJson(data);
// final checkBoxNode = stateTree.root.childAtPath([1, 0]); // final checkBoxNode = document.root.childAtPath([1, 0]);
// expect(checkBoxNode != null, true); // expect(checkBoxNode != null, true);
// final textType = checkBoxNode!.attributes['text-type']; // final textType = checkBoxNode!.attributes['text-type'];
// expect(textType != null, true); // expect(textType != null, true);
@ -27,8 +27,8 @@ void main() {
test('search node by Self in state tree', () async { test('search node by Self in state tree', () async {
// final String response = await rootBundle.loadString('assets/document.json'); // final String response = await rootBundle.loadString('assets/document.json');
// final data = Map<String, Object>.from(json.decode(response)); // final data = Map<String, Object>.from(json.decode(response));
// final stateTree = StateTree.fromJson(data); // final document = StateTree.fromJson(data);
// final checkBoxNode = stateTree.root.childAtPath([1, 0]); // final checkBoxNode = document.root.childAtPath([1, 0]);
// expect(checkBoxNode != null, true); // expect(checkBoxNode != null, true);
// final textType = checkBoxNode!.attributes['text-type']; // final textType = checkBoxNode!.attributes['text-type'];
// expect(textType != null, true); // expect(textType != null, true);
@ -39,21 +39,21 @@ void main() {
test('insert node in state tree', () async { test('insert node in state tree', () async {
// final String response = await rootBundle.loadString('assets/document.json'); // final String response = await rootBundle.loadString('assets/document.json');
// final data = Map<String, Object>.from(json.decode(response)); // final data = Map<String, Object>.from(json.decode(response));
// final stateTree = StateTree.fromJson(data); // final document = StateTree.fromJson(data);
// final insertNode = Node.fromJson({ // final insertNode = Node.fromJson({
// 'type': 'text', // 'type': 'text',
// }); // });
// bool result = stateTree.insert([1, 1], [insertNode]); // bool result = document.insert([1, 1], [insertNode]);
// expect(result, true); // expect(result, true);
// expect(identical(insertNode, stateTree.nodeAtPath([1, 1])), true); // expect(identical(insertNode, document.nodeAtPath([1, 1])), true);
}); });
test('delete node in state tree', () async { test('delete node in state tree', () async {
// final String response = await rootBundle.loadString('assets/document.json'); // final String response = await rootBundle.loadString('assets/document.json');
// final data = Map<String, Object>.from(json.decode(response)); // final data = Map<String, Object>.from(json.decode(response));
// final stateTree = StateTree.fromJson(data); // final document = StateTree.fromJson(data);
// stateTree.delete([1, 1], 1); // document.delete([1, 1], 1);
// final node = stateTree.nodeAtPath([1, 1]); // final node = document.nodeAtPath([1, 1]);
// expect(node != null, true); // expect(node != null, true);
// expect(node!.attributes['tag'], '**'); // expect(node!.attributes['tag'], '**');
}); });
@ -61,10 +61,10 @@ void main() {
test('update node in state tree', () async { test('update node in state tree', () async {
// final String response = await rootBundle.loadString('assets/document.json'); // final String response = await rootBundle.loadString('assets/document.json');
// final data = Map<String, Object>.from(json.decode(response)); // final data = Map<String, Object>.from(json.decode(response));
// final stateTree = StateTree.fromJson(data); // final document = StateTree.fromJson(data);
// final test = stateTree.update([1, 1], {'text-type': 'heading1'}); // final test = document.update([1, 1], {'text-type': 'heading1'});
// expect(test, true); // expect(test, true);
// final updatedNode = stateTree.nodeAtPath([1, 1]); // final updatedNode = document.nodeAtPath([1, 1]);
// expect(updatedNode != null, true); // expect(updatedNode != null, true);
// expect(updatedNode!.attributes['text-type'], 'heading1'); // expect(updatedNode!.attributes['text-type'], 'heading1');
}); });

View File

@ -5,7 +5,7 @@ import 'package:flutter_test/flutter_test.dart';
import 'package:appflowy_editor/src/operation/operation.dart'; import 'package:appflowy_editor/src/operation/operation.dart';
import 'package:appflowy_editor/src/operation/transaction_builder.dart'; import 'package:appflowy_editor/src/operation/transaction_builder.dart';
import 'package:appflowy_editor/src/editor_state.dart'; import 'package:appflowy_editor/src/editor_state.dart';
import 'package:appflowy_editor/src/document/state_tree.dart'; import 'package:appflowy_editor/src/core/state/document.dart';
void main() { void main() {
TestWidgetsFlutterBinding.ensureInitialized(); TestWidgetsFlutterBinding.ensureInitialized();
@ -56,7 +56,7 @@ void main() {
item2, item2,
item3, item3,
])); ]));
final state = EditorState(document: StateTree(root: root)); final state = EditorState(document: Document(root: root));
expect(item1.path, [0]); expect(item1.path, [0]);
expect(item2.path, [1]); expect(item2.path, [1]);
@ -74,7 +74,7 @@ void main() {
group("toJson", () { group("toJson", () {
test("insert", () { test("insert", () {
final root = Node(type: "root", attributes: {}, children: LinkedList()); final root = Node(type: "root", attributes: {}, children: LinkedList());
final state = EditorState(document: StateTree(root: root)); final state = EditorState(document: Document(root: root));
final item1 = Node(type: "node", attributes: {}, children: LinkedList()); final item1 = Node(type: "node", attributes: {}, children: LinkedList());
final tb = TransactionBuilder(state); final tb = TransactionBuilder(state);
@ -100,7 +100,7 @@ void main() {
..addAll([ ..addAll([
item1, item1,
])); ]));
final state = EditorState(document: StateTree(root: root)); final state = EditorState(document: Document(root: root));
final tb = TransactionBuilder(state); final tb = TransactionBuilder(state);
tb.deleteNode(item1); tb.deleteNode(item1);
final transaction = tb.finish(); final transaction = tb.finish();

View File

@ -17,7 +17,7 @@ void main() async {
} }
test("HistoryItem #1", () { test("HistoryItem #1", () {
final document = StateTree(root: _createEmptyEditorRoot()); final document = Document(root: _createEmptyEditorRoot());
final editorState = EditorState(document: document); final editorState = EditorState(document: document);
final historyItem = HistoryItem(); final historyItem = HistoryItem();
@ -35,7 +35,7 @@ void main() async {
}); });
test("HistoryItem #2", () { test("HistoryItem #2", () {
final document = StateTree(root: _createEmptyEditorRoot()); final document = Document(root: _createEmptyEditorRoot());
final editorState = EditorState(document: document); final editorState = EditorState(document: document);
final historyItem = HistoryItem(); final historyItem = HistoryItem();