123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431 |
- use crate::editor::{DocumentNode, DocumentOperation};
- use flowy_error::FlowyResult;
- use lib_ot::core::{AttributeHashMap, DeltaOperation, Insert, Transaction};
- use lib_ot::text_delta::{DeltaTextOperation, DeltaTextOperations};
- pub struct DeltaRevisionMigration();
- impl DeltaRevisionMigration {
- pub fn run(delta: DeltaTextOperations) -> FlowyResult<Transaction> {
- let migrate_background_attribute = |insert: &mut Insert<AttributeHashMap>| {
- if let Some(Some(color)) = insert
- .attributes
- .get("background")
- .map(|value| value.str_value())
- {
- insert.attributes.remove_key("background");
- insert.attributes.insert("backgroundColor", color);
- }
- };
- let migrate_strike_attribute = |insert: &mut Insert<AttributeHashMap>| {
- if let Some(Some(_)) = insert
- .attributes
- .get("strike")
- .map(|value| value.str_value())
- {
- insert.attributes.remove_key("strike");
- insert.attributes.insert("strikethrough", true);
- }
- };
- let migrate_link_attribute = |insert: &mut Insert<AttributeHashMap>| {
- if let Some(Some(link)) = insert.attributes.get("link").map(|value| value.str_value()) {
- insert.attributes.remove_key("link");
- insert.attributes.insert("href", link);
- }
- };
- let migrate_list_attribute =
- |attribute_node: &mut DocumentNode, value: &str, number_list_number: &mut usize| {
- if value == "unchecked" {
- *number_list_number = 0;
- attribute_node.attributes.insert("subtype", "checkbox");
- attribute_node.attributes.insert("checkbox", false);
- }
- if value == "checked" {
- *number_list_number = 0;
- attribute_node.attributes.insert("subtype", "checkbox");
- attribute_node.attributes.insert("checkbox", true);
- }
- if value == "bullet" {
- *number_list_number = 0;
- attribute_node.attributes.insert("subtype", "bulleted-list");
- }
- if value == "ordered" {
- *number_list_number += 1;
- attribute_node.attributes.insert("subtype", "number-list");
- attribute_node
- .attributes
- .insert("number", *number_list_number);
- }
- };
- let generate_new_op_with_double_new_lines = |insert: &mut Insert<AttributeHashMap>| {
- let pattern = "\n\n";
- let mut new_ops = vec![];
- if insert.s.as_str().contains(pattern) {
- let insert_str = insert.s.clone();
- let insert_strings = insert_str.split(pattern).map(|s| s.to_owned());
- for (index, new_s) in insert_strings.enumerate() {
- if index == 0 {
- insert.s = new_s.into();
- } else {
- new_ops.push(DeltaOperation::Insert(Insert {
- s: new_s.into(),
- attributes: AttributeHashMap::default(),
- }));
- }
- }
- }
- new_ops
- };
- let create_text_node = |ops: Vec<DeltaTextOperation>| {
- let mut document_node = DocumentNode::new();
- document_node.node_type = "text".to_owned();
- ops.into_iter().for_each(|op| document_node.delta.add(op));
- document_node
- };
- let transform_op = |mut insert: Insert<AttributeHashMap>| {
- // Rename the attribute name from background to backgroundColor
- migrate_background_attribute(&mut insert);
- migrate_strike_attribute(&mut insert);
- migrate_link_attribute(&mut insert);
- let new_ops = generate_new_op_with_double_new_lines(&mut insert);
- (DeltaOperation::Insert(insert), new_ops)
- };
- let mut index: usize = 0;
- let mut number_list_number = 0;
- let mut editor_node = DocumentNode::new();
- editor_node.node_type = "editor".to_owned();
- let mut transaction = Transaction::new();
- transaction.push_operation(DocumentOperation::Insert {
- path: 0.into(),
- nodes: vec![editor_node],
- });
- let mut iter = delta.ops.into_iter().enumerate();
- while let Some((_, op)) = iter.next() {
- let mut document_node = create_text_node(vec![]);
- let mut split_document_nodes = vec![];
- match op {
- DeltaOperation::Delete(_) => tracing::warn!("Should not contain delete operation"),
- DeltaOperation::Retain(_) => tracing::warn!("Should not contain retain operation"),
- DeltaOperation::Insert(insert) => {
- if insert.s.as_str() != "\n" {
- let (op, new_ops) = transform_op(insert);
- document_node.delta.add(op);
- if !new_ops.is_empty() {
- split_document_nodes.push(create_text_node(new_ops));
- }
- }
- while let Some((_, DeltaOperation::Insert(insert))) = iter.next() {
- if insert.s.as_str() != "\n" {
- let (op, new_ops) = transform_op(insert);
- document_node.delta.add(op);
- if !new_ops.is_empty() {
- split_document_nodes.push(create_text_node(new_ops));
- }
- } else {
- let attribute_node = match split_document_nodes.last_mut() {
- None => &mut document_node,
- Some(split_document_node) => split_document_node,
- };
- if let Some(value) = insert.attributes.get("header") {
- attribute_node.attributes.insert("subtype", "heading");
- if let Some(v) = value.int_value() {
- number_list_number = 0;
- attribute_node
- .attributes
- .insert("heading", format!("h{}", v));
- }
- }
- if insert.attributes.get("blockquote").is_some() {
- attribute_node.attributes.insert("subtype", "quote");
- }
- if let Some(value) = insert.attributes.get("list") {
- if let Some(s) = value.str_value() {
- migrate_list_attribute(attribute_node, &s, &mut number_list_number);
- }
- }
- break;
- }
- }
- },
- }
- let mut operations = vec![document_node];
- operations.extend(split_document_nodes);
- operations.into_iter().for_each(|node| {
- // println!("{}", serde_json::to_string(&node).unwrap());
- let operation = DocumentOperation::Insert {
- path: vec![0, index].into(),
- nodes: vec![node],
- };
- transaction.push_operation(operation);
- index += 1;
- });
- }
- Ok(transaction)
- }
- }
- #[cfg(test)]
- mod tests {
- use crate::editor::Document;
- use crate::services::delta_migration::DeltaRevisionMigration;
- use lib_ot::text_delta::DeltaTextOperations;
- #[test]
- fn transform_delta_to_transaction_test() {
- let delta = DeltaTextOperations::from_json(DELTA_STR).unwrap();
- let transaction = DeltaRevisionMigration::run(delta).unwrap();
- let document = Document::from_transaction(transaction).unwrap();
- let s = document.get_content(true).unwrap();
- assert!(!s.is_empty());
- }
- const DELTA_STR: &str = r#"[
- {
- "insert": "\n👋 Welcome to AppFlowy!"
- },
- {
- "insert": "\n",
- "attributes": {
- "header": 1
- }
- },
- {
- "insert": "\nHere are the basics"
- },
- {
- "insert": "\n",
- "attributes": {
- "header": 2
- }
- },
- {
- "insert": "Click anywhere and just start typing"
- },
- {
- "insert": "\n",
- "attributes": {
- "list": "unchecked"
- }
- },
- {
- "insert": "Highlight",
- "attributes": {
- "background": "$fff2cd"
- }
- },
- {
- "insert": " any text, and use the menu at the bottom to "
- },
- {
- "insert": "style",
- "attributes": {
- "italic": true
- }
- },
- {
- "insert": " "
- },
- {
- "insert": "your",
- "attributes": {
- "bold": true
- }
- },
- {
- "insert": " "
- },
- {
- "insert": "writing",
- "attributes": {
- "underline": true
- }
- },
- {
- "insert": " "
- },
- {
- "insert": "however",
- "attributes": {
- "code": true
- }
- },
- {
- "insert": " "
- },
- {
- "insert": "you",
- "attributes": {
- "strike": true
- }
- },
- {
- "insert": " "
- },
- {
- "insert": "like",
- "attributes": {
- "background": "$e8e0ff"
- }
- },
- {
- "insert": "\n",
- "attributes": {
- "list": "unchecked"
- }
- },
- {
- "insert": "Click "
- },
- {
- "insert": "+ New Page",
- "attributes": {
- "background": "$defff1",
- "bold": true
- }
- },
- {
- "insert": " button at the bottom of your sidebar to add a new page"
- },
- {
- "insert": "\n",
- "attributes": {
- "list": "unchecked"
- }
- },
- {
- "insert": "Click the "
- },
- {
- "insert": "'",
- "attributes": {
- "background": "$defff1"
- }
- },
- {
- "insert": "+",
- "attributes": {
- "background": "$defff1",
- "bold": true
- }
- },
- {
- "insert": "'",
- "attributes": {
- "background": "$defff1"
- }
- },
- {
- "insert": " next to any page title in the sidebar to quickly add a new subpage"
- },
- {
- "insert": "\n",
- "attributes": {
- "list": "unchecked"
- }
- },
- {
- "insert": "\nHave a question? "
- },
- {
- "insert": "\n",
- "attributes": {
- "header": 2
- }
- },
- {
- "insert": "Click the "
- },
- {
- "insert": "'?'",
- "attributes": {
- "background": "$defff1",
- "bold": true
- }
- },
- {
- "insert": " at the bottom right for help and support.\n\nLike AppFlowy? Follow us:"
- },
- {
- "insert": "\n",
- "attributes": {
- "header": 2
- }
- },
- {
- "insert": "GitHub: https://github.com/AppFlowy-IO/appflowy"
- },
- {
- "insert": "\n",
- "attributes": {
- "blockquote": true
- }
- },
- {
- "insert": "Twitter: https://twitter.com/appflowy"
- },
- {
- "insert": "\n",
- "attributes": {
- "blockquote": true
- }
- },
- {
- "insert": "Newsletter: https://www.appflowy.io/blog"
- },
- {
- "insert": "\n",
- "attributes": {
- "blockquote": true
- }
- },
- {
- "insert": "item 1"
- },
- {
- "insert": "\n",
- "attributes": {
- "list": "ordered"
- }
- },
- {
- "insert": "item 2"
- },
- {
- "insert": "\n",
- "attributes": {
- "list": "ordered"
- }
- },
- {
- "insert": "item3"
- },
- {
- "insert": "\n",
- "attributes": {
- "list": "ordered"
- }
- },
- {
- "insert": "appflowy",
- "attributes": {
- "link": "https://www.appflowy.io/"
- }
- }
- ]"#;
- }
|