mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
Merge pull request #859 from AppFlowy-IO/feat/document-model-in-rust
Feat: document model in rust
This commit is contained in:
commit
cebee48248
@ -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
|
||||
|
@ -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()],
|
||||
}
|
||||
|
7
frontend/rust-lib/Cargo.lock
generated
7
frontend/rust-lib/Cargo.lock
generated
@ -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
7
shared-lib/Cargo.lock
generated
@ -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",
|
||||
|
@ -24,6 +24,7 @@ lazy_static = "1.4.0"
|
||||
strum = "0.21"
|
||||
strum_macros = "0.21"
|
||||
bytes = "1.0"
|
||||
indextree = "4.4.0"
|
||||
|
||||
|
||||
[features]
|
||||
|
22
shared-lib/lib-ot/src/core/document/attributes.rs
Normal file
22
shared-lib/lib-ot/src/core/document/attributes.rs
Normal 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)
|
||||
}
|
||||
}
|
211
shared-lib/lib-ot/src/core/document/document.rs
Normal file
211
shared-lib/lib-ot/src/core/document/document.rs
Normal 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(())
|
||||
}
|
||||
}
|
215
shared-lib/lib-ot/src/core/document/document_operation.rs
Normal file
215
shared-lib/lib-ot/src/core/document/document_operation.rs
Normal 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":[]}"#);
|
||||
}
|
||||
}
|
13
shared-lib/lib-ot/src/core/document/mod.rs
Normal file
13
shared-lib/lib-ot/src/core/document/mod.rs
Normal 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::*;
|
48
shared-lib/lib-ot/src/core/document/node.rs
Normal file
48
shared-lib/lib-ot/src/core/document/node.rs
Normal 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(),
|
||||
}
|
||||
}
|
||||
}
|
46
shared-lib/lib-ot/src/core/document/position.rs
Normal file
46
shared-lib/lib-ot/src/core/document/position.rs
Normal 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)
|
||||
}
|
||||
}
|
106
shared-lib/lib-ot/src/core/document/transaction.rs
Normal file
106
shared-lib/lib-ot/src/core/document/transaction.rs
Normal 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)
|
||||
}
|
||||
}
|
@ -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::*;
|
||||
|
@ -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 {
|
||||
|
@ -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);
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user