mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
refactor: rename state_tree to document and move document to core/state
This commit is contained in:
parent
d02c29426e
commit
5e7507c8e7
@ -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!),
|
||||
),
|
||||
|
@ -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';
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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,
|
||||
);
|
||||
|
@ -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();
|
||||
|
@ -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]),
|
||||
);
|
||||
|
@ -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);
|
||||
});
|
||||
});
|
||||
}
|
@ -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;
|
||||
|
@ -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');
|
||||
});
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
|
Loading…
Reference in New Issue
Block a user