Merge pull request #859 from AppFlowy-IO/feat/document-model-in-rust

Feat: document model in rust
This commit is contained in:
Nathan.fooo 2022-08-24 14:40:27 +08:00 committed by GitHub
commit cebee48248
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 839 additions and 14 deletions

View File

@ -2,14 +2,14 @@ import 'package:appflowy_editor/appflowy_editor.dart';
abstract class Operation {
factory Operation.fromJson(Map<String, dynamic> map) {
String t = map["type"] as String;
if (t == "insert-operation") {
String t = map["op"] as String;
if (t == "insert") {
return InsertOperation.fromJson(map);
} else if (t == "update-operation") {
} else if (t == "update") {
return UpdateOperation.fromJson(map);
} else if (t == "delete-operation") {
} else if (t == "delete") {
return DeleteOperation.fromJson(map);
} else if (t == "text-edit-operation") {
} else if (t == "text-edit") {
return TextEditOperation.fromJson(map);
}
@ -51,7 +51,7 @@ class InsertOperation extends Operation {
@override
Map<String, dynamic> toJson() {
return {
"type": "insert-operation",
"op": "insert",
"path": path.toList(),
"nodes": nodes.map((n) => n.toJson()),
};
@ -95,7 +95,7 @@ class UpdateOperation extends Operation {
@override
Map<String, dynamic> toJson() {
return {
"type": "update-operation",
"op": "update",
"path": path.toList(),
"attributes": {...attributes},
"oldAttributes": {...oldAttributes},
@ -132,7 +132,7 @@ class DeleteOperation extends Operation {
@override
Map<String, dynamic> toJson() {
return {
"type": "delete-operation",
"op": "delete",
"path": path.toList(),
"nodes": nodes.map((n) => n.toJson()),
};
@ -171,7 +171,7 @@ class TextEditOperation extends Operation {
@override
Map<String, dynamic> toJson() {
return {
"type": "text-edit-operation",
"op": "text-edit",
"path": path.toList(),
"delta": delta.toJson(),
"invert": inverted.toJson(),
@ -207,10 +207,10 @@ Path transformPath(Path preInsertPath, Path b, [int delta = 1]) {
Operation transformOperation(Operation a, Operation b) {
if (a is InsertOperation) {
final newPath = transformPath(a.path, b.path);
final newPath = transformPath(a.path, b.path, a.nodes.length);
return b.copyWithPath(newPath);
} else if (a is DeleteOperation) {
final newPath = transformPath(a.path, b.path, -1);
final newPath = transformPath(a.path, b.path, -1 * a.nodes.length);
return b.copyWithPath(newPath);
}
// TODO: transform update and textedit

View File

@ -84,7 +84,7 @@ void main() {
expect(transaction.toJson(), {
"operations": [
{
"type": "insert-operation",
"op": "insert",
"path": [0],
"nodes": [item1.toJson()],
}
@ -107,7 +107,7 @@ void main() {
expect(transaction.toJson(), {
"operations": [
{
"type": "delete-operation",
"op": "delete",
"path": [0],
"nodes": [item1.toJson()],
}

View File

@ -1618,6 +1618,12 @@ dependencies = [
"serde",
]
[[package]]
name = "indextree"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42b4b46b3311ebd8e5cd44f6b03b36e0f48a70552cf6b036afcebc5626794066"
[[package]]
name = "instant"
version = "0.1.12"
@ -1766,6 +1772,7 @@ dependencies = [
"bytes",
"dashmap",
"derive_more",
"indextree",
"lazy_static",
"log",
"md5",

7
shared-lib/Cargo.lock generated
View File

@ -741,6 +741,12 @@ dependencies = [
"serde",
]
[[package]]
name = "indextree"
version = "4.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42b4b46b3311ebd8e5cd44f6b03b36e0f48a70552cf6b036afcebc5626794066"
[[package]]
name = "instant"
version = "0.1.12"
@ -809,6 +815,7 @@ dependencies = [
"bytes",
"dashmap",
"derive_more",
"indextree",
"lazy_static",
"log",
"md5",

View File

@ -24,6 +24,7 @@ lazy_static = "1.4.0"
strum = "0.21"
strum_macros = "0.21"
bytes = "1.0"
indextree = "4.4.0"
[features]

View File

@ -0,0 +1,22 @@
use std::collections::HashMap;
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct NodeAttributes(pub HashMap<String, Option<String>>);
impl NodeAttributes {
pub fn new() -> NodeAttributes {
NodeAttributes(HashMap::new())
}
pub fn compose(a: &NodeAttributes, b: &NodeAttributes) -> NodeAttributes {
let mut new_map: HashMap<String, Option<String>> = b.0.clone();
for (key, value) in &a.0 {
if b.0.contains_key(key.as_str()) {
new_map.insert(key.into(), value.clone());
}
}
NodeAttributes(new_map)
}
}

View File

@ -0,0 +1,211 @@
use crate::core::document::position::Position;
use crate::core::{
DocumentOperation, NodeAttributes, NodeData, NodeSubTree, OperationTransform, TextDelta, Transaction,
};
use crate::errors::{ErrorBuilder, OTError, OTErrorCode};
use indextree::{Arena, NodeId};
pub struct DocumentTree {
pub arena: Arena<NodeData>,
pub root: NodeId,
}
impl DocumentTree {
pub fn new() -> DocumentTree {
let mut arena = Arena::new();
let root = arena.new_node(NodeData::new("root".into()));
DocumentTree { arena, root }
}
pub fn node_at_path(&self, position: &Position) -> Option<NodeId> {
if position.is_empty() {
return Some(self.root);
}
let mut iterate_node = self.root;
for id in &position.0 {
let child = self.child_at_index_of_path(iterate_node, id.clone());
iterate_node = match child {
Some(node) => node,
None => return None,
};
}
Some(iterate_node)
}
pub fn path_of_node(&self, node_id: NodeId) -> Position {
let mut path: Vec<usize> = Vec::new();
let mut ancestors = node_id.ancestors(&self.arena);
let mut current_node = node_id;
let mut parent = ancestors.next();
while parent.is_some() {
let parent_node = parent.unwrap();
let counter = self.index_of_node(parent_node, current_node);
path.push(counter);
current_node = parent_node;
parent = ancestors.next();
}
Position(path)
}
fn index_of_node(&self, parent_node: NodeId, child_node: NodeId) -> usize {
let mut counter: usize = 0;
let mut children_iterator = parent_node.children(&self.arena);
let mut node = children_iterator.next();
while node.is_some() {
if node.unwrap() == child_node {
return counter;
}
node = children_iterator.next();
counter += 1;
}
counter
}
fn child_at_index_of_path(&self, at_node: NodeId, index: usize) -> Option<NodeId> {
let children = at_node.children(&self.arena);
let mut counter = 0;
for child in children {
if counter == index {
return Some(child);
}
counter += 1;
}
None
}
pub fn apply(&mut self, transaction: Transaction) -> Result<(), OTError> {
for op in &transaction.operations {
self.apply_op(op)?;
}
Ok(())
}
fn apply_op(&mut self, op: &DocumentOperation) -> Result<(), OTError> {
match op {
DocumentOperation::Insert { path, nodes } => self.apply_insert(path, nodes),
DocumentOperation::Update { path, attributes, .. } => self.apply_update(path, attributes),
DocumentOperation::Delete { path, nodes } => self.apply_delete(path, nodes.len()),
DocumentOperation::TextEdit { path, delta, .. } => self.apply_text_edit(path, delta),
}
}
fn apply_insert(&mut self, path: &Position, nodes: &[Box<NodeSubTree>]) -> Result<(), OTError> {
let parent_path = &path.0[0..(path.0.len() - 1)];
let last_index = path.0[path.0.len() - 1];
let parent_node = self
.node_at_path(&Position(parent_path.to_vec()))
.ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?;
self.insert_child_at_index(parent_node, last_index, nodes.as_ref())
}
fn insert_child_at_index(
&mut self,
parent: NodeId,
index: usize,
insert_children: &[Box<NodeSubTree>],
) -> Result<(), OTError> {
if index == 0 && parent.children(&self.arena).next().is_none() {
self.append_subtree(&parent, insert_children);
return Ok(());
}
let children_length = parent.children(&self.arena).fold(0, |counter, _| counter + 1);
if index == children_length {
self.append_subtree(&parent, insert_children);
return Ok(());
}
let node_to_insert = self
.child_at_index_of_path(parent, index)
.ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?;
self.insert_subtree_before(&node_to_insert, insert_children);
Ok(())
}
// recursive append the subtrees to the node
fn append_subtree(&mut self, parent: &NodeId, insert_children: &[Box<NodeSubTree>]) {
for child in insert_children {
let child_id = self.arena.new_node(child.to_node_data());
parent.append(child_id, &mut self.arena);
self.append_subtree(&child_id, child.children.as_ref());
}
}
fn insert_subtree_before(&mut self, before: &NodeId, insert_children: &[Box<NodeSubTree>]) {
for child in insert_children {
let child_id = self.arena.new_node(child.to_node_data());
before.insert_before(child_id, &mut self.arena);
self.append_subtree(&child_id, child.children.as_ref());
}
}
fn apply_update(&mut self, path: &Position, attributes: &NodeAttributes) -> Result<(), OTError> {
let update_node = self
.node_at_path(path)
.ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?;
let node_data = self.arena.get_mut(update_node).unwrap();
let new_node = {
let old_attributes = &node_data.get().attributes;
let new_attributes = NodeAttributes::compose(&old_attributes, attributes);
NodeData {
attributes: new_attributes,
..node_data.get().clone()
}
};
*node_data.get_mut() = new_node;
Ok(())
}
fn apply_delete(&mut self, path: &Position, len: usize) -> Result<(), OTError> {
let mut update_node = self
.node_at_path(path)
.ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?;
for _ in 0..len {
let next = update_node.following_siblings(&self.arena).next();
update_node.remove_subtree(&mut self.arena);
if let Some(next_id) = next {
update_node = next_id;
} else {
break;
}
}
Ok(())
}
fn apply_text_edit(&mut self, path: &Position, delta: &TextDelta) -> Result<(), OTError> {
let edit_node = self
.node_at_path(path)
.ok_or(ErrorBuilder::new(OTErrorCode::PathNotFound).build())?;
let node_data = self.arena.get_mut(edit_node).unwrap();
let new_delta = if let Some(old_delta) = &node_data.get().delta {
Some(old_delta.compose(delta)?)
} else {
None
};
if let Some(new_delta) = new_delta {
*node_data.get_mut() = NodeData {
delta: Some(new_delta),
..node_data.get().clone()
};
};
Ok(())
}
}

View File

@ -0,0 +1,215 @@
use crate::core::document::position::Position;
use crate::core::{NodeAttributes, NodeSubTree, TextDelta};
#[derive(Clone, serde::Serialize, serde::Deserialize)]
#[serde(tag = "op")]
pub enum DocumentOperation {
#[serde(rename = "insert")]
Insert {
path: Position,
nodes: Vec<Box<NodeSubTree>>,
},
#[serde(rename = "update")]
Update {
path: Position,
attributes: NodeAttributes,
#[serde(rename = "oldAttributes")]
old_attributes: NodeAttributes,
},
#[serde(rename = "delete")]
Delete {
path: Position,
nodes: Vec<Box<NodeSubTree>>,
},
#[serde(rename = "text-edit")]
TextEdit {
path: Position,
delta: TextDelta,
inverted: TextDelta,
},
}
impl DocumentOperation {
pub fn path(&self) -> &Position {
match self {
DocumentOperation::Insert { path, .. } => path,
DocumentOperation::Update { path, .. } => path,
DocumentOperation::Delete { path, .. } => path,
DocumentOperation::TextEdit { path, .. } => path,
}
}
pub fn invert(&self) -> DocumentOperation {
match self {
DocumentOperation::Insert { path, nodes } => DocumentOperation::Delete {
path: path.clone(),
nodes: nodes.clone(),
},
DocumentOperation::Update {
path,
attributes,
old_attributes,
} => DocumentOperation::Update {
path: path.clone(),
attributes: old_attributes.clone(),
old_attributes: attributes.clone(),
},
DocumentOperation::Delete { path, nodes } => DocumentOperation::Insert {
path: path.clone(),
nodes: nodes.clone(),
},
DocumentOperation::TextEdit { path, delta, inverted } => DocumentOperation::TextEdit {
path: path.clone(),
delta: inverted.clone(),
inverted: delta.clone(),
},
}
}
pub fn clone_with_new_path(&self, path: Position) -> DocumentOperation {
match self {
DocumentOperation::Insert { nodes, .. } => DocumentOperation::Insert {
path,
nodes: nodes.clone(),
},
DocumentOperation::Update {
attributes,
old_attributes,
..
} => DocumentOperation::Update {
path,
attributes: attributes.clone(),
old_attributes: old_attributes.clone(),
},
DocumentOperation::Delete { nodes, .. } => DocumentOperation::Delete {
path,
nodes: nodes.clone(),
},
DocumentOperation::TextEdit { delta, inverted, .. } => DocumentOperation::TextEdit {
path,
delta: delta.clone(),
inverted: inverted.clone(),
},
}
}
pub fn transform(a: &DocumentOperation, b: &DocumentOperation) -> DocumentOperation {
match a {
DocumentOperation::Insert { path: a_path, nodes } => {
let new_path = Position::transform(a_path, b.path(), nodes.len() as i64);
b.clone_with_new_path(new_path)
}
DocumentOperation::Delete { path: a_path, nodes } => {
let new_path = Position::transform(a_path, b.path(), nodes.len() as i64);
b.clone_with_new_path(new_path)
}
_ => b.clone(),
}
}
}
#[cfg(test)]
mod tests {
use crate::core::{Delta, DocumentOperation, NodeAttributes, NodeSubTree, Position};
#[test]
fn test_transform_path_1() {
assert_eq!(
{ Position::transform(&Position(vec![0, 1]), &Position(vec![0, 1]), 1) }.0,
vec![0, 2]
);
}
#[test]
fn test_transform_path_2() {
assert_eq!(
{ Position::transform(&Position(vec![0, 1]), &Position(vec![0, 2]), 1) }.0,
vec![0, 3]
);
}
#[test]
fn test_transform_path_3() {
assert_eq!(
{ Position::transform(&Position(vec![0, 1]), &Position(vec![0, 2, 7, 8, 9]), 1) }.0,
vec![0, 3, 7, 8, 9]
);
}
#[test]
fn test_transform_path_not_changed() {
assert_eq!(
{ Position::transform(&Position(vec![0, 1, 2]), &Position(vec![0, 0, 7, 8, 9]), 1) }.0,
vec![0, 0, 7, 8, 9]
);
assert_eq!(
{ Position::transform(&Position(vec![0, 1, 2]), &Position(vec![0, 1]), 1) }.0,
vec![0, 1]
);
assert_eq!(
{ Position::transform(&Position(vec![1, 1]), &Position(vec![1, 0]), 1) }.0,
vec![1, 0]
);
}
#[test]
fn test_transform_delta() {
assert_eq!(
{ Position::transform(&Position(vec![0, 1]), &Position(vec![0, 1]), 5) }.0,
vec![0, 6]
);
}
#[test]
fn test_serialize_insert_operation() {
let insert = DocumentOperation::Insert {
path: Position(vec![0, 1]),
nodes: vec![Box::new(NodeSubTree::new("text"))],
};
let result = serde_json::to_string(&insert).unwrap();
assert_eq!(
result,
r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text","attributes":{}}]}"#
);
}
#[test]
fn test_serialize_insert_sub_trees() {
let insert = DocumentOperation::Insert {
path: Position(vec![0, 1]),
nodes: vec![Box::new(NodeSubTree {
node_type: "text".into(),
attributes: NodeAttributes::new(),
delta: None,
children: vec![Box::new(NodeSubTree::new("text".into()))],
})],
};
let result = serde_json::to_string(&insert).unwrap();
assert_eq!(
result,
r#"{"op":"insert","path":[0,1],"nodes":[{"type":"text","attributes":{},"children":[{"type":"text","attributes":{}}]}]}"#
);
}
#[test]
fn test_serialize_update_operation() {
let insert = DocumentOperation::Update {
path: Position(vec![0, 1]),
attributes: NodeAttributes::new(),
old_attributes: NodeAttributes::new(),
};
let result = serde_json::to_string(&insert).unwrap();
assert_eq!(
result,
r#"{"op":"update","path":[0,1],"attributes":{},"oldAttributes":{}}"#
);
}
#[test]
fn test_serialize_text_edit_operation() {
let insert = DocumentOperation::TextEdit {
path: Position(vec![0, 1]),
delta: Delta::new(),
inverted: Delta::new(),
};
let result = serde_json::to_string(&insert).unwrap();
assert_eq!(result, r#"{"op":"text-edit","path":[0,1],"delta":[],"inverted":[]}"#);
}
}

View File

@ -0,0 +1,13 @@
mod attributes;
mod document;
mod document_operation;
mod node;
mod position;
mod transaction;
pub use attributes::*;
pub use document::*;
pub use document_operation::*;
pub use node::*;
pub use position::*;
pub use transaction::*;

View File

@ -0,0 +1,48 @@
use crate::core::{NodeAttributes, TextDelta};
#[derive(Clone)]
pub struct NodeData {
pub node_type: String,
pub attributes: NodeAttributes,
pub delta: Option<TextDelta>,
}
impl NodeData {
pub fn new(node_type: &str) -> NodeData {
NodeData {
node_type: node_type.into(),
attributes: NodeAttributes::new(),
delta: None,
}
}
}
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct NodeSubTree {
#[serde(rename = "type")]
pub node_type: String,
pub attributes: NodeAttributes,
#[serde(skip_serializing_if = "Option::is_none")]
pub delta: Option<TextDelta>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub children: Vec<Box<NodeSubTree>>,
}
impl NodeSubTree {
pub fn new(node_type: &str) -> NodeSubTree {
NodeSubTree {
node_type: node_type.into(),
attributes: NodeAttributes::new(),
delta: None,
children: Vec::new(),
}
}
pub fn to_node_data(&self) -> NodeData {
NodeData {
node_type: self.node_type.clone(),
attributes: self.attributes.clone(),
delta: self.delta.clone(),
}
}
}

View File

@ -0,0 +1,46 @@
#[derive(Clone, serde::Serialize, serde::Deserialize)]
pub struct Position(pub Vec<usize>);
impl Position {
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn len(&self) -> usize {
self.0.len()
}
}
impl Position {
// delta is default to be 1
pub fn transform(pre_insert_path: &Position, b: &Position, offset: i64) -> Position {
if pre_insert_path.len() > b.len() {
return b.clone();
}
if pre_insert_path.is_empty() || b.is_empty() {
return b.clone();
}
// check the prefix
for i in 0..(pre_insert_path.len() - 1) {
if pre_insert_path.0[i] != b.0[i] {
return b.clone();
}
}
let mut prefix: Vec<usize> = pre_insert_path.0[0..(pre_insert_path.len() - 1)].into();
let mut suffix: Vec<usize> = b.0[pre_insert_path.0.len()..].into();
let prev_insert_last: usize = *pre_insert_path.0.last().unwrap();
let b_at_index = b.0[pre_insert_path.0.len() - 1];
if prev_insert_last <= b_at_index {
prefix.push(((b_at_index as i64) + offset) as usize);
} else {
prefix.push(b_at_index);
}
prefix.append(&mut suffix);
return Position(prefix);
}
}
impl From<Vec<usize>> for Position {
fn from(v: Vec<usize>) -> Self {
Position(v)
}
}

View File

@ -0,0 +1,106 @@
use crate::core::document::position::Position;
use crate::core::{DocumentOperation, DocumentTree, NodeAttributes, NodeSubTree};
use indextree::NodeId;
use std::collections::HashMap;
pub struct Transaction {
pub operations: Vec<DocumentOperation>,
}
impl Transaction {
fn new(operations: Vec<DocumentOperation>) -> Transaction {
Transaction { operations }
}
}
pub struct TransactionBuilder<'a> {
document: &'a DocumentTree,
operations: Vec<DocumentOperation>,
}
impl<'a> TransactionBuilder<'a> {
pub fn new(document: &'a DocumentTree) -> TransactionBuilder {
TransactionBuilder {
document,
operations: Vec::new(),
}
}
pub fn insert_nodes_at_path(&mut self, path: &Position, nodes: &[Box<NodeSubTree>]) {
self.push(DocumentOperation::Insert {
path: path.clone(),
nodes: nodes.to_vec(),
});
}
pub fn update_attributes_at_path(&mut self, path: &Position, attributes: HashMap<String, Option<String>>) {
let mut old_attributes: HashMap<String, Option<String>> = HashMap::new();
let node = self.document.node_at_path(path).unwrap();
let node_data = self.document.arena.get(node).unwrap().get();
for key in attributes.keys() {
let old_attrs = &node_data.attributes;
let old_value = match old_attrs.0.get(key.as_str()) {
Some(value) => value.clone(),
None => None,
};
old_attributes.insert(key.clone(), old_value);
}
self.push(DocumentOperation::Update {
path: path.clone(),
attributes: NodeAttributes(attributes),
old_attributes: NodeAttributes(old_attributes),
})
}
pub fn delete_node_at_path(&mut self, path: &Position) {
self.delete_nodes_at_path(path, 1);
}
pub fn delete_nodes_at_path(&mut self, path: &Position, length: usize) {
let mut node = self.document.node_at_path(path).unwrap();
let mut deleted_nodes: Vec<Box<NodeSubTree>> = Vec::new();
for _ in 0..length {
deleted_nodes.push(self.get_deleted_nodes(node.clone()));
node = node.following_siblings(&self.document.arena).next().unwrap();
}
self.operations.push(DocumentOperation::Delete {
path: path.clone(),
nodes: deleted_nodes,
})
}
fn get_deleted_nodes(&self, node_id: NodeId) -> Box<NodeSubTree> {
let node = self.document.arena.get(node_id.clone()).unwrap();
let node_data = node.get();
let mut children: Vec<Box<NodeSubTree>> = vec![];
let mut children_iterators = node_id.children(&self.document.arena);
loop {
let next_child = children_iterators.next();
if let Some(child_id) = next_child {
children.push(self.get_deleted_nodes(child_id));
} else {
break;
}
}
Box::new(NodeSubTree {
node_type: node_data.node_type.clone(),
attributes: node_data.attributes.clone(),
delta: node_data.delta.clone(),
children,
})
}
pub fn push(&mut self, op: DocumentOperation) {
self.operations.push(op);
}
pub fn finalize(self) -> Transaction {
Transaction::new(self.operations)
}
}

View File

@ -1,9 +1,11 @@
mod delta;
mod document;
mod interval;
mod operation;
mod ot_str;
pub use delta::*;
pub use document::*;
pub use interval::*;
pub use operation::*;
pub use ot_str::*;

View File

@ -60,7 +60,7 @@ impl std::convert::From<Utf8Error> for OTError {
}
}
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Eq, PartialEq)]
pub enum OTErrorCode {
IncompatibleLength,
ApplyInsertFail,
@ -74,6 +74,7 @@ pub enum OTErrorCode {
DuplicatedRevision,
RevisionIDConflict,
Internal,
PathNotFound,
}
pub struct ErrorBuilder {

View File

@ -1 +1,147 @@
use lib_ot::core::{DocumentTree, NodeAttributes, NodeSubTree, Position, TransactionBuilder};
use lib_ot::errors::OTErrorCode;
use std::collections::HashMap;
#[test]
fn main() {
// Create a new arena
let _document = DocumentTree::new();
}
#[test]
fn test_documents() {
let mut document = DocumentTree::new();
let transaction = {
let mut tb = TransactionBuilder::new(&document);
tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]);
tb.finalize()
};
document.apply(transaction).unwrap();
assert!(document.node_at_path(&vec![0].into()).is_some());
let node = document.node_at_path(&vec![0].into()).unwrap();
let node_data = document.arena.get(node).unwrap().get();
assert_eq!(node_data.node_type, "text");
let transaction = {
let mut tb = TransactionBuilder::new(&document);
tb.update_attributes_at_path(
&vec![0].into(),
HashMap::from([("subtype".into(), Some("bullet-list".into()))]),
);
tb.finalize()
};
document.apply(transaction).unwrap();
let transaction = {
let mut tb = TransactionBuilder::new(&document);
tb.delete_node_at_path(&vec![0].into());
tb.finalize()
};
document.apply(transaction).unwrap();
assert!(document.node_at_path(&vec![0].into()).is_none());
}
#[test]
fn test_inserts_nodes() {
let mut document = DocumentTree::new();
let transaction = {
let mut tb = TransactionBuilder::new(&document);
tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]);
tb.insert_nodes_at_path(&vec![1].into(), &vec![Box::new(NodeSubTree::new("text"))]);
tb.insert_nodes_at_path(&vec![2].into(), &vec![Box::new(NodeSubTree::new("text"))]);
tb.finalize()
};
document.apply(transaction).unwrap();
let transaction = {
let mut tb = TransactionBuilder::new(&document);
tb.insert_nodes_at_path(&vec![1].into(), &vec![Box::new(NodeSubTree::new("text"))]);
tb.finalize()
};
document.apply(transaction).unwrap();
}
#[test]
fn test_inserts_subtrees() {
let mut document = DocumentTree::new();
let transaction = {
let mut tb = TransactionBuilder::new(&document);
tb.insert_nodes_at_path(
&vec![0].into(),
&vec![Box::new(NodeSubTree {
node_type: "text".into(),
attributes: NodeAttributes::new(),
delta: None,
children: vec![Box::new(NodeSubTree::new("image".into()))],
})],
);
tb.finalize()
};
document.apply(transaction).unwrap();
let node = document.node_at_path(&Position(vec![0, 0])).unwrap();
let data = document.arena.get(node).unwrap().get();
assert_eq!(data.node_type, "image");
}
#[test]
fn test_update_nodes() {
let mut document = DocumentTree::new();
let transaction = {
let mut tb = TransactionBuilder::new(&document);
tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]);
tb.insert_nodes_at_path(&vec![1].into(), &vec![Box::new(NodeSubTree::new("text"))]);
tb.insert_nodes_at_path(&vec![2].into(), &vec![Box::new(NodeSubTree::new("text"))]);
tb.finalize()
};
document.apply(transaction).unwrap();
let transaction = {
let mut tb = TransactionBuilder::new(&document);
tb.update_attributes_at_path(&vec![1].into(), HashMap::from([("bolded".into(), Some("true".into()))]));
tb.finalize()
};
document.apply(transaction).unwrap();
let node = document.node_at_path(&Position(vec![1])).unwrap();
let node_data = document.arena.get(node).unwrap().get();
let is_bold = node_data.attributes.0.get("bolded").unwrap().clone();
assert_eq!(is_bold.unwrap(), "true");
}
#[test]
fn test_delete_nodes() {
let mut document = DocumentTree::new();
let transaction = {
let mut tb = TransactionBuilder::new(&document);
tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]);
tb.insert_nodes_at_path(&vec![1].into(), &vec![Box::new(NodeSubTree::new("text"))]);
tb.insert_nodes_at_path(&vec![2].into(), &vec![Box::new(NodeSubTree::new("text"))]);
tb.finalize()
};
document.apply(transaction).unwrap();
let transaction = {
let mut tb = TransactionBuilder::new(&document);
tb.delete_node_at_path(&Position(vec![1]));
tb.finalize()
};
document.apply(transaction).unwrap();
let len = document.root.children(&document.arena).fold(0, |count, _| count + 1);
assert_eq!(len, 2);
}
#[test]
fn test_errors() {
let mut document = DocumentTree::new();
let transaction = {
let mut tb = TransactionBuilder::new(&document);
tb.insert_nodes_at_path(&vec![0].into(), &vec![Box::new(NodeSubTree::new("text"))]);
tb.insert_nodes_at_path(&vec![100].into(), &vec![Box::new(NodeSubTree::new("text"))]);
tb.finalize()
};
let result = document.apply(transaction);
assert_eq!(result.err().unwrap().code, OTErrorCode::PathNotFound);
}