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 &&
snapshot.connectionState == ConnectionState.done) {
_editorState ??= EditorState(
document: StateTree.fromJson(
document: Document.fromJson(
Map<String, Object>.from(
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/location/position.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/attributes.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/document/state_tree.dart';
import 'package:appflowy_editor/src/core/state/document.dart';
/// [NodeIterator] is used to traverse the nodes in visual order.
class NodeIterator implements Iterator<Node> {
NodeIterator({
required this.stateTree,
required this.document,
required this.startNode,
this.endNode,
});
final StateTree stateTree;
final Document document;
final Node startNode;
final Node? endNode;

View File

@ -69,4 +69,22 @@ extension PathExtensions on Path {
..removeLast()
..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: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/transaction.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.
class EditorState {
final StateTree document;
final Document document;
// Service reference.
final service = FlowyService();
@ -105,7 +105,7 @@ class EditorState {
}
factory EditorState.empty() {
return EditorState(document: StateTree.empty());
return EditorState(document: Document.empty());
}
/// Apply the transaction to the state.
@ -167,7 +167,7 @@ class EditorState {
} else if (op is DeleteOperation) {
document.delete(op.path, op.nodes.length);
} else if (op is TextEditOperation) {
document.textEdit(op.path, op.delta);
document.updateText(op.path, op.delta);
}
_observer.add(op);
}

View File

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

View File

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

View File

@ -14,7 +14,7 @@ void main() async {
root.insert(node);
}
final nodes = NodeIterator(
stateTree: StateTree(root: root),
document: Document(root: root),
startNode: root.childAtPath([0])!,
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;
Node get root => _editorState.document.root;
StateTree get document => _editorState.document;
Document get document => _editorState.document;
int get documentLength => _editorState.document.root.children.length;
Selection? get documentSelection =>
_editorState.service.selectionService.currentSelection.value;
@ -155,7 +155,7 @@ class EditorWidgetTester {
EditorState _createEmptyDocument() {
return EditorState(
document: StateTree(
document: Document(
root: _createEmptyEditorRoot(),
),
)..disableSealTimer = true;

View File

@ -9,16 +9,16 @@ void main() {
test('create state tree', () async {
// final String response = await rootBundle.loadString('assets/document.json');
// final data = Map<String, Object>.from(json.decode(response));
// final stateTree = StateTree.fromJson(data);
// expect(stateTree.root.type, 'root');
// expect(stateTree.root.toJson(), data['document']);
// final document = StateTree.fromJson(data);
// expect(document.root.type, 'root');
// expect(document.root.toJson(), data['document']);
});
test('search node by Path in state tree', () async {
// final String response = await rootBundle.loadString('assets/document.json');
// final data = Map<String, Object>.from(json.decode(response));
// final stateTree = StateTree.fromJson(data);
// final checkBoxNode = stateTree.root.childAtPath([1, 0]);
// final document = StateTree.fromJson(data);
// final checkBoxNode = document.root.childAtPath([1, 0]);
// expect(checkBoxNode != null, true);
// final textType = checkBoxNode!.attributes['text-type'];
// expect(textType != null, true);
@ -27,8 +27,8 @@ void main() {
test('search node by Self in state tree', () async {
// final String response = await rootBundle.loadString('assets/document.json');
// final data = Map<String, Object>.from(json.decode(response));
// final stateTree = StateTree.fromJson(data);
// final checkBoxNode = stateTree.root.childAtPath([1, 0]);
// final document = StateTree.fromJson(data);
// final checkBoxNode = document.root.childAtPath([1, 0]);
// expect(checkBoxNode != null, true);
// final textType = checkBoxNode!.attributes['text-type'];
// expect(textType != null, true);
@ -39,21 +39,21 @@ void main() {
test('insert node in state tree', () async {
// final String response = await rootBundle.loadString('assets/document.json');
// final data = Map<String, Object>.from(json.decode(response));
// final stateTree = StateTree.fromJson(data);
// final document = StateTree.fromJson(data);
// final insertNode = Node.fromJson({
// 'type': 'text',
// });
// bool result = stateTree.insert([1, 1], [insertNode]);
// bool result = document.insert([1, 1], [insertNode]);
// 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 {
// final String response = await rootBundle.loadString('assets/document.json');
// final data = Map<String, Object>.from(json.decode(response));
// final stateTree = StateTree.fromJson(data);
// stateTree.delete([1, 1], 1);
// final node = stateTree.nodeAtPath([1, 1]);
// final document = StateTree.fromJson(data);
// document.delete([1, 1], 1);
// final node = document.nodeAtPath([1, 1]);
// expect(node != null, true);
// expect(node!.attributes['tag'], '**');
});
@ -61,10 +61,10 @@ void main() {
test('update node in state tree', () async {
// final String response = await rootBundle.loadString('assets/document.json');
// final data = Map<String, Object>.from(json.decode(response));
// final stateTree = StateTree.fromJson(data);
// final test = stateTree.update([1, 1], {'text-type': 'heading1'});
// final document = StateTree.fromJson(data);
// final test = document.update([1, 1], {'text-type': 'heading1'});
// expect(test, true);
// final updatedNode = stateTree.nodeAtPath([1, 1]);
// final updatedNode = document.nodeAtPath([1, 1]);
// expect(updatedNode != null, true);
// 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/transaction_builder.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() {
TestWidgetsFlutterBinding.ensureInitialized();
@ -56,7 +56,7 @@ void main() {
item2,
item3,
]));
final state = EditorState(document: StateTree(root: root));
final state = EditorState(document: Document(root: root));
expect(item1.path, [0]);
expect(item2.path, [1]);
@ -74,7 +74,7 @@ void main() {
group("toJson", () {
test("insert", () {
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 tb = TransactionBuilder(state);
@ -100,7 +100,7 @@ void main() {
..addAll([
item1,
]));
final state = EditorState(document: StateTree(root: root));
final state = EditorState(document: Document(root: root));
final tb = TransactionBuilder(state);
tb.deleteNode(item1);
final transaction = tb.finish();

View File

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