diff --git a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts index bdfd1d4fde345..18d368727b088 100644 --- a/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts +++ b/frontend/appflowy_tauri/src/appflowy_app/stores/reducers/document/async-actions/turn_to.ts @@ -36,7 +36,7 @@ export const turnToBlockThunk = createAsyncThunk( let caretId, caretIndex = caret?.index || 0; const deltaOperator = new BlockDeltaOperator(documentState, controller); - let delta = deltaOperator.getDeltaWithBlockId(node.id); + let delta = deltaOperator.getDeltaWithBlockId(node.id) || new Delta([{ insert: '' }]); // insert new block after current block const insertActions = []; @@ -44,14 +44,14 @@ export const turnToBlockThunk = createAsyncThunk( delta = new Delta([{ insert: node.data.formula }]); } - if (delta && type === BlockType.EquationBlock) { + if (type === BlockType.EquationBlock) { data.formula = deltaOperator.getDeltaText(delta); const block = newBlock(type, parent.id, data); insertActions.push(controller.getInsertAction(block, node.id)); caretId = block.id; caretIndex = 0; - } else if (delta && type === BlockType.DividerBlock) { + } else if (type === BlockType.DividerBlock) { const block = newBlock(type, parent.id, data); insertActions.push(controller.getInsertAction(block, node.id)); @@ -68,7 +68,7 @@ export const turnToBlockThunk = createAsyncThunk( caretId = nodeId; caretIndex = 0; insertActions.push(...actions); - } else if (delta) { + } else { caretId = generateId(); const actions = deltaOperator.getNewTextLineActions({ diff --git a/frontend/rust-lib/event-integration/src/document/document_event.rs b/frontend/rust-lib/event-integration/src/document/document_event.rs index ab20d240bd396..2deb791064f4f 100644 --- a/frontend/rust-lib/event-integration/src/document/document_event.rs +++ b/frontend/rust-lib/event-integration/src/document/document_event.rs @@ -4,6 +4,9 @@ use serde_json::Value; use flowy_document2::entities::*; use flowy_document2::event_map::DocumentEvent; +use flowy_document2::parser::parser_entities::{ + ConvertDocumentPayloadPB, ConvertDocumentResponsePB, +}; use flowy_folder2::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB}; use flowy_folder2::event_map::FolderEvent; @@ -108,6 +111,19 @@ impl DocumentEventTest { .await; } + pub async fn convert_document( + &self, + payload: ConvertDocumentPayloadPB, + ) -> ConvertDocumentResponsePB { + let core = &self.inner; + EventBuilder::new(core.clone()) + .event(DocumentEvent::ConvertDocument) + .payload(payload) + .async_send() + .await + .parse::() + } + pub async fn create_text(&self, payload: TextDeltaPayloadPB) { let core = &self.inner; EventBuilder::new(core.clone()) diff --git a/frontend/rust-lib/event-integration/tests/document/local_test/test.rs b/frontend/rust-lib/event-integration/tests/document/local_test/test.rs index 0016a9118cd4d..b03320f247b8e 100644 --- a/frontend/rust-lib/event-integration/tests/document/local_test/test.rs +++ b/frontend/rust-lib/event-integration/tests/document/local_test/test.rs @@ -2,6 +2,7 @@ use collab_document::blocks::json_str_to_hashmap; use event_integration::document::document_event::DocumentEventTest; use event_integration::document::utils::*; use flowy_document2::entities::*; +use flowy_document2::parser::parser_entities::{ConvertDocumentPayloadPB, ExportTypePB}; use serde_json::{json, Value}; use std::collections::HashMap; @@ -120,3 +121,35 @@ async fn apply_text_delta_test() { json!([{ "insert": "Hello! World" }]).to_string() ); } + +macro_rules! generate_convert_document_test_cases { + ($($json:ident, $text:ident, $html:ident),*) => { + [ + $((ExportTypePB { json: $json, text: $text, html: $html }, ($json, $text, $html))),* + ] + }; +} + +#[tokio::test] +async fn convert_document_test() { + let test = DocumentEventTest::new().await; + let view = test.create_document().await; + + let test_cases = generate_convert_document_test_cases! { + true, true, true, + false, true, true, + false, false, false + }; + + for (export_types, (json_assert, text_assert, html_assert)) in test_cases.iter() { + let copy_payload = ConvertDocumentPayloadPB { + document_id: view.id.to_string(), + range: None, + export_types: export_types.clone(), + }; + let result = test.convert_document(copy_payload).await; + assert_eq!(result.json.is_some(), *json_assert); + assert_eq!(result.text.is_some(), *text_assert); + assert_eq!(result.html.is_some(), *html_assert); + } +} diff --git a/frontend/rust-lib/flowy-document2/Flowy.toml b/frontend/rust-lib/flowy-document2/Flowy.toml index a48035cb147c8..6ef51c220dd89 100644 --- a/frontend/rust-lib/flowy-document2/Flowy.toml +++ b/frontend/rust-lib/flowy-document2/Flowy.toml @@ -1,3 +1,3 @@ # Check out the FlowyConfig (located in flowy_toml.rs) for more details. -proto_input = ["src/event_map.rs", "src/entities.rs", "src/notification.rs"] +proto_input = ["src/event_map.rs", "src/entities.rs", "src/notification.rs", "src/parser/parser_entities.rs"] event_files = ["src/event_map.rs"] \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/src/event_handler.rs b/frontend/rust-lib/flowy-document2/src/event_handler.rs index c3b10859d03d1..a576caf697ebe 100644 --- a/frontend/rust-lib/flowy-document2/src/event_handler.rs +++ b/frontend/rust-lib/flowy-document2/src/event_handler.rs @@ -15,6 +15,11 @@ use flowy_error::{FlowyError, FlowyResult}; use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult}; use crate::entities::*; +use crate::parser::document_data_parser::DocumentDataParser; +use crate::parser::parser_entities::{ + ConvertDocumentParams, ConvertDocumentPayloadPB, ConvertDocumentResponsePB, +}; + use crate::{manager::DocumentManager, parser::json::parser::JsonToDocumentParser}; fn upgrade_document( @@ -303,3 +308,45 @@ impl From<(&Vec, bool)> for DocEventPB { } } } + +/** +* Handler for converting a document to a JSON string, HTML string, or plain text string. + +* @param data: AFPluginData<[ConvertDocumentPayloadPB]> + +* @param manager: AFPluginState> + +* @return DataResult<[ConvertDocumentResponsePB], FlowyError> + */ +pub async fn convert_document( + data: AFPluginData, + manager: AFPluginState>, +) -> DataResult { + let manager = upgrade_document(manager)?; + let params: ConvertDocumentParams = data.into_inner().try_into()?; + + let document = manager.get_document(¶ms.document_id).await?; + let document_data = document.lock().get_document_data()?; + let parser = DocumentDataParser::new(Arc::new(document_data), params.range); + + if !params.export_types.any_enabled() { + return data_result_ok(ConvertDocumentResponsePB::default()); + } + + let root = &parser.to_json(); + + data_result_ok(ConvertDocumentResponsePB { + json: params + .export_types + .json + .then(|| serde_json::to_string(root).unwrap_or_default()), + html: params + .export_types + .html + .then(|| parser.to_html_with_json(root)), + text: params + .export_types + .text + .then(|| parser.to_text_with_json(root)), + }) +} diff --git a/frontend/rust-lib/flowy-document2/src/event_map.rs b/frontend/rust-lib/flowy-document2/src/event_map.rs index c9ff9569d6d28..e7c4dcd13ffb6 100644 --- a/frontend/rust-lib/flowy-document2/src/event_map.rs +++ b/frontend/rust-lib/flowy-document2/src/event_map.rs @@ -5,6 +5,7 @@ use strum_macros::Display; use flowy_derive::{Flowy_Event, ProtoBuf_Enum}; use lib_dispatch::prelude::AFPlugin; +use crate::event_handler::convert_document; use crate::event_handler::get_snapshot_handler; use crate::{event_handler::*, manager::DocumentManager}; @@ -27,6 +28,7 @@ pub fn init(document_manager: Weak) -> AFPlugin { .event(DocumentEvent::GetDocumentSnapshots, get_snapshot_handler) .event(DocumentEvent::CreateText, create_text_handler) .event(DocumentEvent::ApplyTextDeltaEvent, apply_text_delta_handler) + .event(DocumentEvent::ConvertDocument, convert_document) } #[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)] @@ -76,4 +78,49 @@ pub enum DocumentEvent { #[event(input = "TextDeltaPayloadPB")] ApplyTextDeltaEvent = 11, + + /// Handler for converting a document to a JSON string, HTML string, or plain text string. + /// + /// ConvertDocumentPayloadPB is the input of this event. + /// ConvertDocumentResponsePB is the output of this event. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```txt + /// // document: [{ "block_id": "1", "type": "paragraph", "data": {"delta": [{ "insert": "Hello World!" }] } }, { "block_id": "2", "type": "paragraph", "data": {"delta": [{ "insert": "Hello World!" }] } + /// let test = DocumentEventTest::new().await; + /// let view = test.create_document().await; + /// let payload = ConvertDocumentPayloadPB { + /// document_id: view.id, + /// range: Some(RangePB { + /// start: SelectionPB { + /// block_id: "1".to_string(), + /// index: 0, + /// length: 5, + /// }, + /// end: SelectionPB { + /// block_id: "2".to_string(), + /// index: 5, + /// length: 7, + /// } + /// }), + /// export_types: ConvertTypePB { + /// json: true, + /// text: true, + /// html: true, + /// }, + /// }; + /// let result = test.convert_document(payload).await; + /// assert_eq!(result.json, Some("[{ \"block_id\": \"1\", \"type\": \"paragraph\", \"data\": {\"delta\": [{ \"insert\": \"Hello\" }] } }, { \"block_id\": \"2\", \"type\": \"paragraph\", \"data\": {\"delta\": [{ \"insert\": \" World!\" }] } }".to_string())); + /// assert_eq!(result.text, Some("Hello\n World!".to_string())); + /// assert_eq!(result.html, Some("

Hello

World!

".to_string())); + /// ``` + /// # + #[event( + input = "ConvertDocumentPayloadPB", + output = "ConvertDocumentResponsePB" + )] + ConvertDocument = 12, } diff --git a/frontend/rust-lib/flowy-document2/src/parser/constant.rs b/frontend/rust-lib/flowy-document2/src/parser/constant.rs new file mode 100644 index 0000000000000..d5c4d56e6b748 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/src/parser/constant.rs @@ -0,0 +1,37 @@ +pub const DELTA: &str = "delta"; +pub const LEVEL: &str = "level"; +pub const NUMBER: &str = "number"; +pub const CHECKED: &str = "checked"; + +pub const COLLAPSED: &str = "collapsed"; +pub const LANGUAGE: &str = "language"; + +pub const ICON: &str = "icon"; +pub const WIDTH: &str = "width"; +pub const HEIGHT: &str = "height"; +pub const URL: &str = "url"; +pub const CAPTION: &str = "caption"; +pub const ALIGN: &str = "align"; + +pub const PAGE: &str = "page"; +pub const HEADING: &str = "heading"; +pub const PARAGRAPH: &str = "paragraph"; +pub const NUMBERED_LIST: &str = "numbered_list"; +pub const BULLETED_LIST: &str = "bulleted_list"; +pub const TODO_LIST: &str = "todo_list"; +pub const TOGGLE_LIST: &str = "toggle_list"; +pub const QUOTE: &str = "quote"; +pub const CALLOUT: &str = "callout"; +pub const IMAGE: &str = "image"; +pub const DIVIDER: &str = "divider"; +pub const MATH_EQUATION: &str = "math_equation"; +pub const BOLD: &str = "bold"; +pub const ITALIC: &str = "italic"; +pub const STRIKETHROUGH: &str = "strikethrough"; +pub const CODE: &str = "code"; +pub const UNDERLINE: &str = "underline"; +pub const FONT_COLOR: &str = "font_color"; +pub const BG_COLOR: &str = "bg_color"; +pub const HREF: &str = "href"; +pub const FORMULA: &str = "formula"; +pub const MENTION: &str = "mention"; diff --git a/frontend/rust-lib/flowy-document2/src/parser/document_data_parser.rs b/frontend/rust-lib/flowy-document2/src/parser/document_data_parser.rs new file mode 100644 index 0000000000000..5339c7eff30e9 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/src/parser/document_data_parser.rs @@ -0,0 +1,180 @@ +use crate::parser::parser_entities::{ConvertBlockToHtmlParams, NestedBlock, Range}; +use crate::parser::utils::{ + block_to_nested_json, get_delta_for_block, get_delta_for_selection, get_flat_block_ids, + ConvertBlockToJsonParams, +}; +use collab_document::blocks::DocumentData; +use std::collections::HashMap; +use std::sync::Arc; + +/// DocumentDataParser is a struct for parsing a document's data and converting it to JSON, HTML, or text. +pub struct DocumentDataParser { + /// The document data to parse. + pub document_data: Arc, + /// The range of the document data to parse. If the range is None, the entire document data will be parsed. + pub range: Option, +} + +impl DocumentDataParser { + pub fn new(document_data: Arc, range: Option) -> Self { + Self { + document_data, + range, + } + } + + /// Converts the JSON to an HTML representation. + pub fn to_html_with_json(&self, json: &Option) -> String { + let mut html = String::new(); + html.push_str(""); + if let Some(json) = json { + let params = ConvertBlockToHtmlParams { + prev_block_ty: None, + next_block_ty: None, + }; + html.push_str(json.convert_to_html(params).as_str()); + } + html + } + + /// Converts the JSON to plain text. + pub fn to_text_with_json(&self, json: &Option) -> String { + if let Some(json) = json { + json.convert_to_text() + } else { + String::new() + } + } + + /// Converts the document data to HTML. + pub fn to_html(&self) -> String { + let json = self.to_json(); + self.to_html_with_json(&json) + } + + /// Converts the document data to plain text. + pub fn to_text(&self) -> String { + let json = self.to_json(); + self.to_text_with_json(&json) + } + + /// Converts the document data to a nested JSON structure, considering the optional range. + pub fn to_json(&self) -> Option { + let root_id = &self.document_data.page_id; + // flatten the block id list. + let block_id_list = get_flat_block_ids(root_id, &self.document_data); + + // collect the block ids in the range. + let mut in_range_block_ids = self.collect_in_range_block_ids(&block_id_list); + // insert the root block id if it is not in the in-range block ids. + if !in_range_block_ids.contains(root_id) { + in_range_block_ids.push(root_id.to_string()); + } + + // build the parameters for converting the block to JSON with the in-range block ids. + let convert_params = self.build_convert_json_params(&in_range_block_ids); + // convert the root block to JSON. + let mut root = block_to_nested_json(root_id, &convert_params)?; + + // If the start block's parent is outside the in-range selection, we need to insert the start block. + if self.should_insert_start_block() { + self.insert_start_block_json(&mut root, &convert_params); + } + + Some(root) + } + + /// Collects the block ids in the range. + fn collect_in_range_block_ids(&self, block_id_list: &Vec) -> Vec { + if let Some(range) = &self.range { + // Find the positions of start and end block IDs in the list + let mut start_index = block_id_list + .iter() + .position(|id| id == &range.start.block_id) + .unwrap_or(0); + let mut end_index = block_id_list + .iter() + .position(|id| id == &range.end.block_id) + .unwrap_or(0); + + if start_index > end_index { + // Swap start and end if they are in reverse order + std::mem::swap(&mut start_index, &mut end_index); + } + + // Slice the block IDs based on the positions of start and end + block_id_list[start_index..=end_index].to_vec() + } else { + // If no range is specified, return the entire list + block_id_list.to_owned() + } + } + + /// Builds the parameters for converting the block to JSON. + /// ConvertBlockToJsonParams format: + /// { + /// blocks: HashMap>, // in-range blocks + /// relation_map: HashMap>>, // in-range blocks' children + /// delta_map: HashMap, // in-range blocks' delta + /// } + fn build_convert_json_params(&self, block_id_list: &[String]) -> ConvertBlockToJsonParams { + let mut delta_map = HashMap::new(); + let mut in_range_blocks = HashMap::new(); + let mut relation_map = HashMap::new(); + + for block_id in block_id_list { + if let Some(block) = self.document_data.blocks.get(block_id) { + // Insert the block into the in-range block map. + in_range_blocks.insert(block_id.to_string(), Arc::new(block.to_owned())); + + // If the block has children, insert the children into the relation map. + if let Some(children) = self.document_data.meta.children_map.get(&block.children) { + relation_map.insert(block_id.to_string(), Arc::new(children.to_owned())); + } + + let delta = match &self.range { + Some(range) if block_id == &range.start.block_id => { + get_delta_for_selection(&range.start, &self.document_data) + }, + Some(range) if block_id == &range.end.block_id => { + get_delta_for_selection(&range.end, &self.document_data) + }, + _ => get_delta_for_block(block_id, &self.document_data), + }; + + // If the delta exists, insert it into the delta map. + if let Some(delta) = delta { + delta_map.insert(block_id.to_string(), delta); + } + } + } + + ConvertBlockToJsonParams { + blocks: in_range_blocks, + relation_map, + delta_map, + } + } + + // Checks if the start block should be inserted whether the start block's parent is outside the in-range selection. + fn should_insert_start_block(&self) -> bool { + if let Some(range) = &self.range { + if let Some(start_block) = self.document_data.blocks.get(&range.start.block_id) { + return start_block.parent != self.document_data.page_id; + } + } + false + } + + // Inserts the start block JSON to the root JSON. + fn insert_start_block_json( + &self, + root: &mut NestedBlock, + convert_params: &ConvertBlockToJsonParams, + ) { + let start = &self.range.as_ref().unwrap().start; + if let Some(start_block_json) = block_to_nested_json(&start.block_id, convert_params) { + root.children.insert(0, start_block_json); + } + } +} diff --git a/frontend/rust-lib/flowy-document2/src/parser/mod.rs b/frontend/rust-lib/flowy-document2/src/parser/mod.rs index 22fdbb38c88f6..0c040e6e51348 100644 --- a/frontend/rust-lib/flowy-document2/src/parser/mod.rs +++ b/frontend/rust-lib/flowy-document2/src/parser/mod.rs @@ -1 +1,5 @@ +pub mod constant; +pub mod document_data_parser; pub mod json; +pub mod parser_entities; +pub mod utils; diff --git a/frontend/rust-lib/flowy-document2/src/parser/parser_entities.rs b/frontend/rust-lib/flowy-document2/src/parser/parser_entities.rs new file mode 100644 index 0000000000000..0fec927dcd11c --- /dev/null +++ b/frontend/rust-lib/flowy-document2/src/parser/parser_entities.rs @@ -0,0 +1,481 @@ +use crate::parse::NotEmptyStr; +use crate::parser::constant::{ + BG_COLOR, BOLD, BULLETED_LIST, CALLOUT, CHECKED, CODE, DELTA, DIVIDER, FONT_COLOR, FORMULA, + HEADING, HREF, ICON, IMAGE, ITALIC, LANGUAGE, LEVEL, MATH_EQUATION, NUMBERED_LIST, PAGE, + PARAGRAPH, QUOTE, STRIKETHROUGH, TODO_LIST, TOGGLE_LIST, UNDERLINE, URL, +}; +use crate::parser::utils::{ + convert_insert_delta_from_json, convert_nested_block_children_to_html, delta_to_html, + delta_to_text, +}; +use flowy_derive::ProtoBuf; +use flowy_error::ErrorCode; +use serde::{Deserialize, Serialize}; +use serde_json::Value; +use std::collections::HashMap; +use std::sync::Arc; + +#[derive(Default, ProtoBuf)] +pub struct SelectionPB { + #[pb(index = 1)] + pub block_id: String, + + #[pb(index = 2)] + pub index: u32, + + #[pb(index = 3)] + pub length: u32, +} + +#[derive(Default, ProtoBuf)] +pub struct RangePB { + #[pb(index = 1)] + pub start: SelectionPB, + + #[pb(index = 2)] + pub end: SelectionPB, +} + +/** +* ExportTypePB + * @field json: bool // export json data + * @field html: bool // export html data + * @field text: bool // export text data + */ +#[derive(Default, ProtoBuf, Debug, Clone)] +pub struct ExportTypePB { + #[pb(index = 1)] + pub json: bool, + + #[pb(index = 2)] + pub html: bool, + + #[pb(index = 3)] + pub text: bool, +} +/** +* ConvertDocumentPayloadPB + * @field document_id: String + * @file range: Option - optional // if range is None, copy the whole document + * @field export_types: [ExportTypePB] + */ +#[derive(Default, ProtoBuf)] +pub struct ConvertDocumentPayloadPB { + #[pb(index = 1)] + pub document_id: String, + + #[pb(index = 2, one_of)] + pub range: Option, + + #[pb(index = 3)] + pub export_types: ExportTypePB, +} + +#[derive(Default, ProtoBuf, Debug)] +pub struct ConvertDocumentResponsePB { + #[pb(index = 1, one_of)] + pub json: Option, + #[pb(index = 2, one_of)] + pub html: Option, + #[pb(index = 3, one_of)] + pub text: Option, +} + +pub struct Selection { + pub block_id: String, + pub index: u32, + pub length: u32, +} + +pub struct Range { + pub start: Selection, + pub end: Selection, +} + +pub struct ExportType { + pub json: bool, + pub html: bool, + pub text: bool, +} + +pub struct ConvertDocumentParams { + pub document_id: String, + pub range: Option, + pub export_types: ExportType, +} + +impl ExportType { + pub fn any_enabled(&self) -> bool { + self.json || self.html || self.text + } +} + +impl From for Selection { + fn from(data: SelectionPB) -> Self { + Selection { + block_id: data.block_id, + index: data.index, + length: data.length, + } + } +} + +impl From for Range { + fn from(data: RangePB) -> Self { + Range { + start: data.start.into(), + end: data.end.into(), + } + } +} + +impl From for ExportType { + fn from(data: ExportTypePB) -> Self { + ExportType { + json: data.json, + html: data.html, + text: data.text, + } + } +} +impl TryInto for ConvertDocumentPayloadPB { + type Error = ErrorCode; + fn try_into(self) -> Result { + let document_id = + NotEmptyStr::parse(self.document_id).map_err(|_| ErrorCode::DocumentIdIsEmpty)?; + let range = self.range.map(|data| data.into()); + + Ok(ConvertDocumentParams { + document_id: document_id.0, + range, + export_types: self.export_types.into(), + }) + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct InsertDelta { + #[serde(default)] + pub insert: String, + #[serde(default)] + pub attributes: Option>, +} + +impl InsertDelta { + pub fn to_text(&self) -> String { + self.insert.clone() + } + + pub fn to_html(&self) -> String { + let mut html = String::new(); + let mut style = String::new(); + // If there are attributes, serialize them as a HashMap. + if let Some(attrs) = &self.attributes { + // Serialize the font color attributes. + if let Some(color) = attrs.get(FONT_COLOR) { + style.push_str(&format!( + "color: {};", + color.to_string().replace("0x", "#").trim_matches('\"') + )); + } + // Serialize the background color attributes. + if let Some(color) = attrs.get(BG_COLOR) { + style.push_str(&format!( + "background-color: {};", + color.to_string().replace("0x", "#").trim_matches('\"') + )); + } + // Serialize the href attributes. + if let Some(href) = attrs.get(HREF) { + html.push_str(&format!("", href)); + } + + // Serialize the code attributes. + if let Some(code) = attrs.get(CODE) { + if code.as_bool().unwrap_or(false) { + html.push_str(""); + } + } + // Serialize the italic, underline, strikethrough, bold, formula attributes. + if let Some(italic) = attrs.get(ITALIC) { + if italic.as_bool().unwrap_or(false) { + style.push_str("font-style: italic;"); + } + } + if let Some(underline) = attrs.get(UNDERLINE) { + if underline.as_bool().unwrap_or(false) { + style.push_str("text-decoration: underline;"); + } + } + if let Some(strikethrough) = attrs.get(STRIKETHROUGH) { + if strikethrough.as_bool().unwrap_or(false) { + style.push_str("text-decoration: line-through;"); + } + } + if let Some(bold) = attrs.get(BOLD) { + if bold.as_bool().unwrap_or(false) { + style.push_str("font-weight: bold;"); + } + } + if let Some(formula) = attrs.get(FORMULA) { + if formula.as_bool().unwrap_or(false) { + style.push_str("font-family: fantasy;"); + } + } + } + // Serialize the attributes to style. + if !style.is_empty() { + html.push_str(&format!("", style)); + } + // Serialize the insert field. + html.push_str(&self.insert); + + // Close the style tag. + if !style.is_empty() { + html.push_str(""); + } + // Close the tags: , . + if let Some(attrs) = &self.attributes { + if attrs.contains_key(HREF) { + html.push_str(""); + } + if attrs.contains_key(CODE) { + html.push_str(""); + } + } + html + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NestedBlock { + #[serde(default)] + pub id: String, + #[serde(rename = "type")] + pub ty: String, + #[serde(default)] + pub data: HashMap, + #[serde(default)] + pub children: Vec, +} + +impl Eq for NestedBlock {} + +impl PartialEq for NestedBlock { + // ignore the id field + fn eq(&self, other: &Self) -> bool { + self.ty == other.ty + && self.data.iter().all(|(k, v)| { + let other_v = other.data.get(k).unwrap_or(&Value::Null); + if k == DELTA { + let v = convert_insert_delta_from_json(v); + let other_v = convert_insert_delta_from_json(other_v); + return v == other_v; + } + v == other_v + }) + && self.children == other.children + } +} + +pub struct ConvertBlockToHtmlParams { + pub prev_block_ty: Option, + pub next_block_ty: Option, +} + +impl NestedBlock { + pub fn new( + id: String, + ty: String, + data: HashMap, + children: Vec, + ) -> Self { + Self { + id, + ty, + data, + children, + } + } + + pub fn add_child(&mut self, child: NestedBlock) { + self.children.push(child); + } + + pub fn convert_to_html(&self, params: ConvertBlockToHtmlParams) -> String { + let mut html = String::new(); + + let text_html = self + .data + .get("delta") + .and_then(convert_insert_delta_from_json) + .map(|delta| delta_to_html(&delta)) + .unwrap_or_default(); + + let prev_block_ty = params.prev_block_ty.unwrap_or_default(); + let next_block_ty = params.next_block_ty.unwrap_or_default(); + + match self.ty.as_str() { + HEADING => { + let level = self.data.get(LEVEL).unwrap_or(&Value::Null); + if level.as_u64().unwrap_or(0) > 6 { + html.push_str(&format!("
{}
", text_html)); + } else { + html.push_str(&format!("{}", level, text_html, level)); + } + }, + PARAGRAPH => { + html.push_str(&format!("

{}

", text_html)); + html.push_str(&convert_nested_block_children_to_html(Arc::new( + self.to_owned(), + ))); + }, + CALLOUT => { + html.push_str(&format!( + "

{}{}

", + self + .data + .get(ICON) + .unwrap_or(&Value::Null) + .to_string() + .trim_matches('\"'), + text_html + )); + }, + IMAGE => { + html.push_str(&format!( + "{}", + self.data.get(URL).unwrap(), + "AppFlowy-Image" + )); + }, + DIVIDER => { + html.push_str("
"); + }, + MATH_EQUATION => { + let formula = self.data.get(FORMULA).unwrap_or(&Value::Null); + html.push_str(&format!( + "

{}

", + formula.to_string().trim_matches('\"') + )); + }, + CODE => { + let language = self.data.get(LANGUAGE).unwrap_or(&Value::Null); + html.push_str(&format!( + "
{}
", + language.to_string().trim_matches('\"'), + text_html + )); + }, + BULLETED_LIST | NUMBERED_LIST | TODO_LIST | TOGGLE_LIST => { + let list_type = match self.ty.as_str() { + BULLETED_LIST => "ul", + NUMBERED_LIST => "ol", + TODO_LIST => "ul", + TOGGLE_LIST => "ul", + _ => "ul", // Default to "ul" for unknown types + }; + if prev_block_ty != self.ty { + html.push_str(&format!("<{}>", list_type)); + } + if self.ty == TODO_LIST { + let checked_str = if self + .data + .get(CHECKED) + .and_then(|checked| checked.as_bool()) + .unwrap_or(false) + { + "x" + } else { + " " + }; + html.push_str(&format!("
  • [{}] {}
  • ", checked_str, text_html)); + } else { + html.push_str(&format!("
  • {}
  • ", text_html)); + } + + html.push_str(&convert_nested_block_children_to_html(Arc::new( + self.to_owned(), + ))); + + if next_block_ty != self.ty { + html.push_str(&format!("", list_type)); + } + }, + + QUOTE => { + if prev_block_ty != self.ty { + html.push_str("
    "); + } + html.push_str(&format!("

    {}

    ", text_html)); + html.push_str(&convert_nested_block_children_to_html(Arc::new( + self.to_owned(), + ))); + if next_block_ty != self.ty { + html.push_str("
    "); + } + }, + PAGE => { + if !text_html.is_empty() { + html.push_str(&format!("

    {}

    ", text_html)); + } + html.push_str(&convert_nested_block_children_to_html(Arc::new( + self.to_owned(), + ))); + }, + _ => { + html.push_str(&format!("

    {}

    ", text_html)); + html.push_str(&convert_nested_block_children_to_html(Arc::new( + self.to_owned(), + ))); + }, + }; + + html + } + + pub fn convert_to_text(&self) -> String { + let mut text = String::new(); + + let delta_text = self + .data + .get("delta") + .and_then(convert_insert_delta_from_json) + .map(|delta| delta_to_text(&delta)) + .unwrap_or_default(); + + match self.ty.as_str() { + CALLOUT => { + text.push_str(&format!( + "{}{}\n", + self + .data + .get(ICON) + .unwrap_or(&Value::Null) + .to_string() + .trim_matches('\"'), + delta_text + )); + }, + MATH_EQUATION => { + let formula = self.data.get(FORMULA).unwrap_or(&Value::Null); + text.push_str(&format!("{}\n", formula.to_string().trim_matches('\"'))); + }, + PAGE => { + if !delta_text.is_empty() { + text.push_str(&format!("{}\n", delta_text)); + } + for child in &self.children { + text.push_str(&child.convert_to_text()); + } + }, + _ => { + text.push_str(&format!("{}\n", delta_text)); + for child in &self.children { + text.push_str(&child.convert_to_text()); + } + }, + }; + text + } +} diff --git a/frontend/rust-lib/flowy-document2/src/parser/utils.rs b/frontend/rust-lib/flowy-document2/src/parser/utils.rs new file mode 100644 index 0000000000000..0897164e70521 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/src/parser/utils.rs @@ -0,0 +1,167 @@ +use crate::parser::constant::DELTA; +use crate::parser::parser_entities::{ + ConvertBlockToHtmlParams, InsertDelta, NestedBlock, Selection, +}; +use collab_document::blocks::{Block, DocumentData}; +use serde_json::Value; +use std::collections::HashMap; +use std::sync::Arc; + +pub struct ConvertBlockToJsonParams { + pub(crate) blocks: HashMap>, + pub(crate) relation_map: HashMap>>, + pub(crate) delta_map: HashMap>, +} +pub fn block_to_nested_json( + block_id: &str, + convert_params: &ConvertBlockToJsonParams, +) -> Option { + let blocks = &convert_params.blocks; + let relation_map = &convert_params.relation_map; + let delta_map = &convert_params.delta_map; + // Attempt to retrieve the block using the block_id + let block = blocks.get(block_id)?; + + // Retrieve the children for this block from the relation map + let children = relation_map.get(&block.id)?; + + // Recursively convert children blocks to JSON + let children: Vec<_> = children + .iter() + .filter_map(|child_id| block_to_nested_json(child_id, convert_params)) + .collect(); + + // Clone block data + let mut data = block.data.clone(); + + // Insert delta into data if available + if let Some(delta) = delta_map.get(&block.id) { + if let Ok(delta_value) = serde_json::to_value(delta) { + data.insert(DELTA.to_string(), delta_value); + } + } + + // Create and return the NestedBlock + Some(NestedBlock { + id: block.id.to_string(), + ty: block.ty.to_string(), + children, + data, + }) +} + +pub fn get_flat_block_ids(block_id: &str, data: &DocumentData) -> Vec { + let blocks = &data.blocks; + let children_map = &data.meta.children_map; + + if let Some(block) = blocks.get(block_id) { + let mut result = vec![block.id.clone()]; + + if let Some(child_ids) = children_map.get(&block.children) { + for child_id in child_ids { + let child_blocks = get_flat_block_ids(child_id, data); + result.extend(child_blocks); + } + + return result; + } + } + + vec![] +} + +pub fn get_delta_for_block(block_id: &str, data: &DocumentData) -> Option> { + let text_map = data.meta.text_map.as_ref()?; // Retrieve the text_map reference + + data.blocks.get(block_id).and_then(|block| { + let text_id = block.external_id.as_ref()?; + let delta_str = text_map.get(text_id)?; + serde_json::from_str::>(delta_str).ok() + }) +} + +pub fn get_delta_for_selection( + selection: &Selection, + data: &DocumentData, +) -> Option> { + let delta = get_delta_for_block(&selection.block_id, data)?; + let start = selection.index as usize; + let end = (selection.index + selection.length) as usize; + Some(slice_delta(&delta, start, end)) +} + +pub fn slice_delta(delta: &Vec, start: usize, end: usize) -> Vec { + let mut result = vec![]; + let mut index = 0; + for d in delta { + let content = &d.insert; + let text_len = content.len(); + // skip if index is not reached + if index + text_len <= start { + index += text_len; + continue; + } + // break if index is over end + if index >= end { + break; + } + // slice content, and push to result + let start_offset = std::cmp::max(0, start as isize - index as isize) as usize; + let end_offset = std::cmp::min(end - index, text_len); + let content = content[start_offset..end_offset].to_string(); + result.push(InsertDelta { + insert: content, + attributes: d.attributes.clone(), + }); + + index += text_len; + } + result +} +pub fn delta_to_text(delta: &Vec) -> String { + let mut result = String::new(); + for d in delta { + result.push_str(d.to_text().as_str()); + } + result +} + +pub fn delta_to_html(delta: &Vec) -> String { + let mut result = String::new(); + for d in delta { + result.push_str(d.to_html().as_str()); + } + result +} + +pub fn convert_nested_block_children_to_html(block: Arc) -> String { + let children = &block.children; + let mut html = String::new(); + let num_children = children.len(); + + for (i, child) in children.iter().enumerate() { + let prev_block_ty = if i > 0 { + Some(children[i - 1].ty.to_string()) + } else { + None + }; + + let next_block_ty = if i + 1 < num_children { + Some(children[i + 1].ty.to_string()) + } else { + None + }; + + let child_html = child.convert_to_html(ConvertBlockToHtmlParams { + prev_block_ty, + next_block_ty, + }); + + html.push_str(&child_html); + } + html +} + +pub fn convert_insert_delta_from_json(delta_value: &Value) -> Option> { + serde_json::from_value::>(delta_value.to_owned()).ok() +} diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/bulleted_list.html b/frontend/rust-lib/flowy-document2/tests/assets/html/bulleted_list.html new file mode 100644 index 0000000000000..ae621dac0bbae --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/bulleted_list.html @@ -0,0 +1 @@ +
    • Highlight
    • You can also

      • nest
    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/callout.html b/frontend/rust-lib/flowy-document2/tests/assets/html/callout.html new file mode 100644 index 0000000000000..14e7c5d4e7d14 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/callout.html @@ -0,0 +1,6 @@ +

    🥰 +Like AppFlowy? Follow us: +GitHub +Twitter: @appflowy +Newsletter +

    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/code.html b/frontend/rust-lib/flowy-document2/tests/assets/html/code.html new file mode 100644 index 0000000000000..9d859c1c5fc1d --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/code.html @@ -0,0 +1,5 @@ +
    // This is the main function.
    +fn main() {
    +    // Print text to the console.
    +    println!("Hello World!");
    +}
    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/divider.html b/frontend/rust-lib/flowy-document2/tests/assets/html/divider.html new file mode 100644 index 0000000000000..95ca67339a4ed --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/divider.html @@ -0,0 +1 @@ +
    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/heading.html b/frontend/rust-lib/flowy-document2/tests/assets/html/heading.html new file mode 100644 index 0000000000000..99459dee6e464 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/heading.html @@ -0,0 +1 @@ +

    Heading1

    Heading2

    Heading3

    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/image.html b/frontend/rust-lib/flowy-document2/tests/assets/html/image.html new file mode 100644 index 0000000000000..24908700a51b1 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/image.html @@ -0,0 +1 @@ +AppFlowy-Image \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/math_equation.html b/frontend/rust-lib/flowy-document2/tests/assets/html/math_equation.html new file mode 100644 index 0000000000000..38f572ec822f6 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/math_equation.html @@ -0,0 +1 @@ +

    E = MC^2

    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/numbered_list.html b/frontend/rust-lib/flowy-document2/tests/assets/html/numbered_list.html new file mode 100644 index 0000000000000..7bcc0ec06b98c --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/numbered_list.html @@ -0,0 +1 @@ +
    1. Highlight
    2. You can also

      1. nest
    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/paragraph.html b/frontend/rust-lib/flowy-document2/tests/assets/html/paragraph.html new file mode 100644 index 0000000000000..786d48fa50606 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/paragraph.html @@ -0,0 +1,6 @@ +

    +Like AppFlowy? Follow us: +GitHub +Twitter: @appflowy +Newsletter +

    Click ? at the bottom right for help and support.

    Highlight any text, and use the editing menu to style your writing however you like.1+1=2

    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/quote.html b/frontend/rust-lib/flowy-document2/tests/assets/html/quote.html new file mode 100644 index 0000000000000..6da59e8aeb27d --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/quote.html @@ -0,0 +1 @@ +

    This is a quote

    This is a paragraph

    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/todo_list.html b/frontend/rust-lib/flowy-document2/tests/assets/html/todo_list.html new file mode 100644 index 0000000000000..19f48f2410017 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/todo_list.html @@ -0,0 +1 @@ +
    • [x] Highlight
    • You can also

      • [ ] nest
    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/html/toggle_list.html b/frontend/rust-lib/flowy-document2/tests/assets/html/toggle_list.html new file mode 100644 index 0000000000000..a8e93bdf74ea8 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/html/toggle_list.html @@ -0,0 +1 @@ +
    • Click ? at the bottom right for help and support.
    • This is a paragraph

      • This is a toggle list
    \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/bulleted_list.json b/frontend/rust-lib/flowy-document2/tests/assets/json/bulleted_list.json new file mode 100644 index 0000000000000..47080498057c4 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/bulleted_list.json @@ -0,0 +1,37 @@ +{ + "type": "page", + "children": [ + { + "type": "bulleted_list", + "data": { + "delta": [ + { + "insert": "Highlight" + } + ] + }, + "children": [ + { + "type": "paragraph", + "data": { + "delta": [ + { + "insert": "You can also" + } + ] + } + }, + { + "type": "bulleted_list", + "data": { + "delta": [ + { + "insert": "nest" + } + ] + } + } + ] + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/callout.json b/frontend/rust-lib/flowy-document2/tests/assets/json/callout.json new file mode 100644 index 0000000000000..a494982f6472b --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/callout.json @@ -0,0 +1,32 @@ +{ + "type": "page", + "data": {}, + "children": [ + { + "type": "callout", + "data": { + "delta": [ + { "insert": "\nLike AppFlowy? Follow us:\n" }, + { + "attributes": { + "href": "https://github.com/AppFlowy-IO/AppFlowy" + }, + "insert": "GitHub" + }, + { "insert": "\n" }, + { + "attributes": { "href": "https://twitter.com/appflowy" }, + "insert": "Twitter" + }, + { "insert": ": @appflowy\n" }, + { + "attributes": { "href": "https://blog-appflowy.ghost.io/" }, + "insert": "Newsletter" + }, + { "insert": "\n" } + ], + "icon": "🥰" + } + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/code.json b/frontend/rust-lib/flowy-document2/tests/assets/json/code.json new file mode 100644 index 0000000000000..21bf6379077f3 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/code.json @@ -0,0 +1,14 @@ +{ + "type": "page", + "children": [{ + "type": "code", + "data": { + "language": "rust", + "delta": [ + { + "insert": "// This is the main function.\nfn main() {\n // Print text to the console.\n println!(\"Hello World!\");\n}" + } + ] + } + }] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/divider.json b/frontend/rust-lib/flowy-document2/tests/assets/json/divider.json new file mode 100644 index 0000000000000..05625723f3016 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/divider.json @@ -0,0 +1,10 @@ +{ + "type": "page", + "data": {}, + "children": [ + { + "type": "divider", + "data": {} + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/heading.json b/frontend/rust-lib/flowy-document2/tests/assets/json/heading.json new file mode 100644 index 0000000000000..a1c9ffcb783fe --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/heading.json @@ -0,0 +1,39 @@ +{ + "type": "page", + "data": {}, + "children": [ + { + "type": "heading", + "data": { + "level": 1, + "delta": [ + { + "insert": "Heading1" + } + ] + } + }, + { + "type": "heading", + "data": { + "level": 2, + "delta": [ + { + "insert": "Heading2" + } + ] + } + }, + { + "type": "heading", + "data": { + "level": 3, + "delta": [ + { + "insert": "Heading3" + } + ] + } + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/image.json b/frontend/rust-lib/flowy-document2/tests/assets/json/image.json new file mode 100644 index 0000000000000..0b88529538a5e --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/image.json @@ -0,0 +1,15 @@ +{ + "type": "page", + "data": {}, + "children": [ + { + "type": "image", + "data": { + "url": "https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png", + "width": 272, + "height": 92, + "align": "center" + } + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/initial_document.json b/frontend/rust-lib/flowy-document2/tests/assets/json/initial_document.json new file mode 100644 index 0000000000000..5ef653ad9528b --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/initial_document.json @@ -0,0 +1,267 @@ +{ + "type": "page", + "data": {}, + "children": [ + { + "type": "heading", + "data": { "delta": [{ "insert": "Welcome to AppFlowy!" }], "level": 1 } + }, + { + "type": "heading", + "data": { "delta": [{ "insert": "Here are the basics" }], "level": 2 } + }, + { + "type": "heading", + "data": { "delta": [{ "insert": "Here is H3" }], "level": 3 } + }, + { + "type": "todo_list", + "data": { + "delta": [{ "insert": "Click anywhere and just start typing." }], + "checked": false + }, + "children": [ + { + "type": "todo_list", + "data": { + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "Enter" }, + { "insert": " to create a new line." } + ], + "checked": false + } + } + ] + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { + "attributes": { "bg_color": "0x4dffeb3b" }, + "insert": "Highlight " + }, + { "insert": "any text, and use the editing menu to " }, + { "attributes": { "italic": true }, "insert": "style" }, + { "insert": " " }, + { "attributes": { "bold": true }, "insert": "your" }, + { "insert": " " }, + { "attributes": { "underline": true }, "insert": "writing" }, + { "insert": " " }, + { "attributes": { "code": true }, "insert": "however" }, + { "insert": " you " }, + { "attributes": { "strikethrough": true }, "insert": "like." } + ] + } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { "insert": "As soon as you type " }, + { + "attributes": { "code": true, "font_color": "0xff00b5ff" }, + "insert": "/" + }, + { "insert": " a menu will pop up. Select " }, + { + "attributes": { "bg_color": "0x4d9c27b0" }, + "insert": "different types" + }, + { "insert": " of content blocks you can add." } + ] + } + }, + { + "type": "todo_list", + "data": { + "delta": [ + { "insert": "Type " }, + { "attributes": { "code": true }, "insert": "/" }, + { "insert": " followed by " }, + { "attributes": { "code": true }, "insert": "/bullet" }, + { "insert": " or " }, + { "attributes": { "code": true }, "insert": "/num" }, + { "attributes": { "code": false }, "insert": " to create a list." } + ], + "checked": false + } + }, + { + "type": "todo_list", + "data": { + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "+ New Page " }, + { + "insert": "button at the bottom of your sidebar to add a new page." + } + ], + "checked": true + } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "+" }, + { "insert": " next to any page title in the sidebar to " }, + { + "attributes": { "font_color": "0xff8427e0" }, + "insert": "quickly" + }, + { "insert": " add a new subpage, " }, + { "attributes": { "code": true }, "insert": "Document" }, + { "attributes": { "code": false }, "insert": ", " }, + { "attributes": { "code": true }, "insert": "Grid" }, + { "attributes": { "code": false }, "insert": ", or " }, + { "attributes": { "code": true }, "insert": "Kanban Board" }, + { "attributes": { "code": false }, "insert": "." } + ] + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { "type": "divider" }, + { "type": "paragraph", "data": { "delta": [] } }, + { + "type": "heading", + "data": { + "delta": [{ "insert": "Keyboard shortcuts, markdown, and code block" }], + "level": 2 + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { "insert": "Keyboard shortcuts " }, + { + "attributes": { + "href": "https://appflowy.gitbook.io/docs/essential-documentation/shortcuts" + }, + "insert": "guide" + } + ] + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { "insert": "Markdown " }, + { + "attributes": { + "href": "https://appflowy.gitbook.io/docs/essential-documentation/markdown" + }, + "insert": "reference" + } + ] + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { "insert": "Type " }, + { "attributes": { "code": true }, "insert": "/code" }, + { + "attributes": { "code": false }, + "insert": " to insert a code block" + } + ] + } + }, + { + "type": "code", + "data": { + "language": "rust", + "delta": [ + { + "insert": "// This is the main function.\nfn main() {\n // Print text to the console.\n println!(\"Hello World!\");\n}" + } + ] + } + }, + { + "type": "paragraph", + "data": { "delta": [] }, + "children": [{ + "type": "paragraph", + "data": { "delta": [{ "insert": "This is a paragraph" }] }, + "children": [{ + "type": "paragraph", + "data": { "delta": [{ "insert": "This is a paragraph" }] } + }] + }] + }, + { + "type": "heading", + "data": { "level": 2, "delta": [{ "insert": "Have a question❓" }] } + }, + { + "type": "toggle_list", + "data": { + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "?" }, + { "insert": " at the bottom right for help and support." } + ] + }, + "children": [ + { + "type": "paragraph", + "data": { "delta": [{ "insert": "This is a paragraph" }] } + }, + { + "type": "paragraph", + "data": { "delta": [{ "insert": "This is a paragraph" }] } + } + ] + }, + { + "type": "quote", + "data": { + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "?" }, + { "insert": " at the bottom right for help and support." } + ] + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { + "type": "callout", + "data": { + "delta": [ + { "insert": "\nLike AppFlowy? Follow us:\n" }, + { + "attributes": { + "href": "https://github.com/AppFlowy-IO/AppFlowy" + }, + "insert": "GitHub" + }, + { "insert": "\n" }, + { + "attributes": { "href": "https://twitter.com/appflowy" }, + "insert": "Twitter" + }, + { "insert": ": @appflowy\n" }, + { + "attributes": { "href": "https://blog-appflowy.ghost.io/" }, + "insert": "Newsletter" + }, + { "insert": "\n" } + ], + "icon": "🥰" + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { "type": "paragraph", "data": { "delta": [] } }, + { "type": "paragraph", "data": { "delta": [] } } + ] +} diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/math_equation.json b/frontend/rust-lib/flowy-document2/tests/assets/json/math_equation.json new file mode 100644 index 0000000000000..8d1fd52456399 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/math_equation.json @@ -0,0 +1,9 @@ +{ + "type": "page", + "children": [{ + "type": "math_equation", + "data": { + "formula": "E = MC^2" + } + }] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/numbered_list.json b/frontend/rust-lib/flowy-document2/tests/assets/json/numbered_list.json new file mode 100644 index 0000000000000..cdcea264c738f --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/numbered_list.json @@ -0,0 +1,37 @@ +{ + "type": "page", + "children": [ + { + "type": "numbered_list", + "data": { + "delta": [ + { + "insert": "Highlight" + } + ] + }, + "children": [ + { + "type": "paragraph", + "data": { + "delta": [ + { + "insert": "You can also" + } + ] + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { + "insert": "nest" + } + ] + } + } + ] + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/paragraph.json b/frontend/rust-lib/flowy-document2/tests/assets/json/paragraph.json new file mode 100644 index 0000000000000..50aac23910d87 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/paragraph.json @@ -0,0 +1,59 @@ +{ + "type": "page", + "data": {}, + "children": [ + { + "type": "paragraph", + "data": { "delta": [ + { "insert": "\nLike AppFlowy? Follow us:\n" }, + { + "attributes": { + "href": "https://github.com/AppFlowy-IO/AppFlowy" + }, + "insert": "GitHub" + }, + { "insert": "\n" }, + { + "attributes": { "href": "https://twitter.com/appflowy" }, + "insert": "Twitter" + }, + { "insert": ": @appflowy\n" }, + { + "attributes": { "href": "https://blog-appflowy.ghost.io/" }, + "insert": "Newsletter" + }, + { "insert": "\n" } + ]}, + "children": [{ + "type": "paragraph", + "data": { + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "?" }, + { "insert": " at the bottom right for help and support." } + ] + } + }] + }, + { + "type": "paragraph", + "data": { "delta": [ + { + "attributes": { "bg_color": "0x4dffeb3b" }, + "insert": "Highlight " + }, + { "insert": "any text, and use the editing menu to " }, + { "attributes": { "italic": true }, "insert": "style" }, + { "insert": " " }, + { "attributes": { "bold": true }, "insert": "your" }, + { "insert": " " }, + { "attributes": { "underline": true }, "insert": "writing" }, + { "insert": " " }, + { "attributes": { "code": true }, "insert": "however" }, + { "insert": " you ", "attributes": { "font_color": "0x4dffeb3b" } }, + { "attributes": { "strikethrough": true }, "insert": "like." }, + { "attributes": { "formula": true }, "insert": "1+1=2" } + ] } + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/quote.json b/frontend/rust-lib/flowy-document2/tests/assets/json/quote.json new file mode 100644 index 0000000000000..a17f3d55b78d8 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/quote.json @@ -0,0 +1,25 @@ +{ + "type": "page", + "children": [ + { + "type": "quote", + "data": { + "delta": [ + { + "insert": "This is a quote" + } + ] + }, + "children": [{ + "type": "paragraph", + "data": { + "delta": [ + { + "insert": "This is a paragraph" + } + ] + } + }] + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/range_1.json b/frontend/rust-lib/flowy-document2/tests/assets/json/range_1.json new file mode 100644 index 0000000000000..778a5e5f1dddd --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/range_1.json @@ -0,0 +1,100 @@ +{ + "type": "page", + "data": {}, + "children": [ + { + "type": "heading", + "data": { "delta": [{ "insert": " are the basics" }], "level": 2 } + }, + { + "type": "heading", + "data": { "delta": [{ "insert": "Here is H3" }], "level": 3 } + }, + { + "type": "todo_list", + "data": { + "delta": [{ "insert": "Click anywhere and just start typing." }], + "checked": false + }, + "children": [ + { + "type": "todo_list", + "data": { + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "Enter" }, + { "insert": " to create a new line." } + ], + "checked": false + } + } + ] + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { + "attributes": { "bg_color": "0x4dffeb3b" }, + "insert": "Highlight " + }, + { "insert": "any text, and use the editing menu to " }, + { "attributes": { "italic": true }, "insert": "style" }, + { "insert": " " }, + { "attributes": { "bold": true }, "insert": "your" }, + { "insert": " " }, + { "attributes": { "underline": true }, "insert": "writing" }, + { "insert": " " }, + { "attributes": { "code": true }, "insert": "however" }, + { "insert": " you " }, + { "attributes": { "strikethrough": true }, "insert": "like." } + ] + } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { "insert": "As soon as you type " }, + { + "attributes": { "code": true, "font_color": "0xff00b5ff" }, + "insert": "/" + }, + { "insert": " a menu will pop up. Select " }, + { + "attributes": { "bg_color": "0x4d9c27b0" }, + "insert": "different types" + }, + { "insert": " of content blocks you can add." } + ] + } + }, + { + "type": "todo_list", + "data": { + "delta": [ + { "insert": "Type " }, + { "attributes": { "code": true }, "insert": "/" }, + { "insert": " followed by " }, + { "attributes": { "code": true }, "insert": "/bullet" }, + { "insert": " or " }, + { "attributes": { "code": true }, "insert": "/num" }, + { "attributes": { "code": false }, "insert": " to create a list." } + ], + "checked": false + } + }, + { + "type": "todo_list", + "data": { + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "+ New" } + ], + "checked": true + } + } + ] +} diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/range_2.json b/frontend/rust-lib/flowy-document2/tests/assets/json/range_2.json new file mode 100644 index 0000000000000..3e0740427453a --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/range_2.json @@ -0,0 +1,178 @@ +{ + "type": "page", + "data": {}, + "children": [ + { + "type": "todo_list", + "data": { + "delta": [ + { "attributes": { "code": true }, "insert": "Enter" }, + { "insert": " to create a new line." } + ], + "checked": false + } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { + "attributes": { "bg_color": "0x4dffeb3b" }, + "insert": "Highlight " + }, + { "insert": "any text, and use the editing menu to " }, + { "attributes": { "italic": true }, "insert": "style" }, + { "insert": " " }, + { "attributes": { "bold": true }, "insert": "your" }, + { "insert": " " }, + { "attributes": { "underline": true }, "insert": "writing" }, + { "insert": " " }, + { "attributes": { "code": true }, "insert": "however" }, + { "insert": " you " }, + { "attributes": { "strikethrough": true }, "insert": "like." } + ] + } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { "insert": "As soon as you type " }, + { + "attributes": { "code": true, "font_color": "0xff00b5ff" }, + "insert": "/" + }, + { "insert": " a menu will pop up. Select " }, + { + "attributes": { "bg_color": "0x4d9c27b0" }, + "insert": "different types" + }, + { "insert": " of content blocks you can add." } + ] + } + }, + { + "type": "todo_list", + "data": { + "delta": [ + { "insert": "Type " }, + { "attributes": { "code": true }, "insert": "/" }, + { "insert": " followed by " }, + { "attributes": { "code": true }, "insert": "/bullet" }, + { "insert": " or " }, + { "attributes": { "code": true }, "insert": "/num" }, + { "attributes": { "code": false }, "insert": " to create a list." } + ], + "checked": false + } + }, + { + "type": "todo_list", + "data": { + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "+ New Page " }, + { + "insert": "button at the bottom of your sidebar to add a new page." + } + ], + "checked": true + } + }, + { + "type": "todo_list", + "data": { + "checked": false, + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "+" }, + { "insert": " next to any page title in the sidebar to " }, + { + "attributes": { "font_color": "0xff8427e0" }, + "insert": "quickly" + }, + { "insert": " add a new subpage, " }, + { "attributes": { "code": true }, "insert": "Document" }, + { "attributes": { "code": false }, "insert": ", " }, + { "attributes": { "code": true }, "insert": "Grid" }, + { "attributes": { "code": false }, "insert": ", or " }, + { "attributes": { "code": true }, "insert": "Kanban Board" }, + { "attributes": { "code": false }, "insert": "." } + ] + } + }, + { "type": "paragraph", "data": { "delta": [] } }, + { "type": "divider" }, + { "type": "paragraph", "data": { "delta": [] } }, + { + "type": "heading", + "data": { + "delta": [{ "insert": "Keyboard shortcuts, markdown, and code block" }], + "level": 2 + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { "insert": "Keyboard shortcuts " }, + { + "attributes": { + "href": "https://appflowy.gitbook.io/docs/essential-documentation/shortcuts" + }, + "insert": "guide" + } + ] + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { "insert": "Markdown " }, + { + "attributes": { + "href": "https://appflowy.gitbook.io/docs/essential-documentation/markdown" + }, + "insert": "reference" + } + ] + } + }, + { + "type": "numbered_list", + "data": { + "delta": [ + { "insert": "Type " }, + { "attributes": { "code": true }, "insert": "/code" }, + { + "attributes": { "code": false }, + "insert": " to insert a code block" + } + ] + } + }, + { + "type": "code", + "data": { + "language": "rust", + "delta": [ + { + "insert": "// This is the main function.\nfn main() {\n // Print text to the console.\n println!(\"Hello World!\");\n}" + } + ] + } + }, + { + "type": "paragraph", + "data": { "delta": [] }, + "children": [{ + "type": "paragraph", + "data": { "delta": [{ "insert": "This is a p" }] }, + "children": [] + }] + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/todo_list.json b/frontend/rust-lib/flowy-document2/tests/assets/json/todo_list.json new file mode 100644 index 0000000000000..e37e103af3677 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/todo_list.json @@ -0,0 +1,39 @@ +{ + "type": "page", + "children": [ + { + "type": "todo_list", + "data": { + "checked": true, + "delta": [ + { + "insert": "Highlight" + } + ] + }, + "children": [ + { + "type": "paragraph", + "data": { + "delta": [ + { + "insert": "You can also" + } + ] + } + }, + { + "type": "todo_list", + "checked": false, + "data": { + "delta": [ + { + "insert": "nest" + } + ] + } + } + ] + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/json/toggle_list.json b/frontend/rust-lib/flowy-document2/tests/assets/json/toggle_list.json new file mode 100644 index 0000000000000..89530afd15e90 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/json/toggle_list.json @@ -0,0 +1,25 @@ +{ + "type": "page", + "children": [ + { + "type": "toggle_list", + "data": { + "delta": [ + { "insert": "Click " }, + { "attributes": { "code": true }, "insert": "?" }, + { "insert": " at the bottom right for help and support." } + ] + }, + "children": [ + { + "type": "paragraph", + "data": { "delta": [{ "insert": "This is a paragraph" }] } + }, + { + "type": "toggle_list", + "data": { "delta": [{ "insert": "This is a toggle list" }] } + } + ] + } + ] +} \ No newline at end of file diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/bulleted_list.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/bulleted_list.txt new file mode 100644 index 0000000000000..59fc99d7fe1e3 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/bulleted_list.txt @@ -0,0 +1,3 @@ +Highlight +You can also +nest diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/callout.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/callout.txt new file mode 100644 index 0000000000000..779f4f9f81f0b --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/callout.txt @@ -0,0 +1,6 @@ +🥰 +Like AppFlowy? Follow us: +GitHub +Twitter: @appflowy +Newsletter + diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/code.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/code.txt new file mode 100644 index 0000000000000..9271ac6c895a2 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/code.txt @@ -0,0 +1,5 @@ +// This is the main function. +fn main() { + // Print text to the console. + println!("Hello World!"); +} diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/divider.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/divider.txt new file mode 100644 index 0000000000000..8b137891791fe --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/divider.txt @@ -0,0 +1 @@ + diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/heading.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/heading.txt new file mode 100644 index 0000000000000..45fba2b330701 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/heading.txt @@ -0,0 +1,3 @@ +Heading1 +Heading2 +Heading3 diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/image.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/image.txt new file mode 100644 index 0000000000000..8b137891791fe --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/image.txt @@ -0,0 +1 @@ + diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/math_equation.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/math_equation.txt new file mode 100644 index 0000000000000..ba201486b5341 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/math_equation.txt @@ -0,0 +1 @@ +E = MC^2 diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/numbered_list.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/numbered_list.txt new file mode 100644 index 0000000000000..59fc99d7fe1e3 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/numbered_list.txt @@ -0,0 +1,3 @@ +Highlight +You can also +nest diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/paragraph.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/paragraph.txt new file mode 100644 index 0000000000000..893fbd1a7101d --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/paragraph.txt @@ -0,0 +1,8 @@ + +Like AppFlowy? Follow us: +GitHub +Twitter: @appflowy +Newsletter + +Click ? at the bottom right for help and support. +Highlight any text, and use the editing menu to style your writing however you like.1+1=2 diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/quote.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/quote.txt new file mode 100644 index 0000000000000..e082baf25eee3 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/quote.txt @@ -0,0 +1,2 @@ +This is a quote +This is a paragraph diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/todo_list.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/todo_list.txt new file mode 100644 index 0000000000000..59fc99d7fe1e3 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/todo_list.txt @@ -0,0 +1,3 @@ +Highlight +You can also +nest diff --git a/frontend/rust-lib/flowy-document2/tests/assets/text/toggle_list.txt b/frontend/rust-lib/flowy-document2/tests/assets/text/toggle_list.txt new file mode 100644 index 0000000000000..30369415c04b6 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/assets/text/toggle_list.txt @@ -0,0 +1,3 @@ +Click ? at the bottom right for help and support. +This is a paragraph +This is a toggle list diff --git a/frontend/rust-lib/flowy-document2/tests/document/mod.rs b/frontend/rust-lib/flowy-document2/tests/document/mod.rs index e975a80c55a2c..8d724a938beb4 100644 --- a/frontend/rust-lib/flowy-document2/tests/document/mod.rs +++ b/frontend/rust-lib/flowy-document2/tests/document/mod.rs @@ -2,4 +2,4 @@ mod document_insert_test; mod document_redo_undo_test; mod document_test; mod event_handler_test; -mod util; +pub mod util; diff --git a/frontend/rust-lib/flowy-document2/tests/parser/document_data_parser_test.rs b/frontend/rust-lib/flowy-document2/tests/parser/document_data_parser_test.rs new file mode 100644 index 0000000000000..67a189603165b --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/parser/document_data_parser_test.rs @@ -0,0 +1,105 @@ +use collab_document::blocks::DocumentData; +use flowy_document2::parser::document_data_parser::DocumentDataParser; +use flowy_document2::parser::json::parser::JsonToDocumentParser; +use flowy_document2::parser::parser_entities::{NestedBlock, Range, Selection}; +use std::sync::Arc; + +#[tokio::test] +async fn document_data_parse_json_test() { + let initial_json_str = include_str!("../assets/json/initial_document.json"); + let document_data = JsonToDocumentParser::json_str_to_document(initial_json_str) + .unwrap() + .into(); + let parser = DocumentDataParser::new(Arc::new(document_data), None); + let read_me_json = serde_json::from_str::(initial_json_str).unwrap(); + let json = parser.to_json().unwrap(); + assert_eq!(read_me_json, json); +} + +// range_1 is a range from the 2nd block to the 8th block +#[tokio::test] +async fn document_data_to_json_with_range_1_test() { + let initial_json_str = include_str!("../assets/json/initial_document.json"); + let document_data: DocumentData = JsonToDocumentParser::json_str_to_document(initial_json_str) + .unwrap() + .into(); + + let children_map = &document_data.meta.children_map; + let page_block_id = &document_data.page_id; + let blocks = &document_data.blocks; + let page_block = blocks.get(page_block_id).unwrap(); + let children = children_map.get(page_block.children.as_str()).unwrap(); + + let range = Range { + start: Selection { + block_id: children.get(1).unwrap().to_string(), + index: 4, + length: 15, + }, + end: Selection { + block_id: children.get(7).unwrap().to_string(), + index: 0, + length: 11, + }, + }; + let parser = DocumentDataParser::new(Arc::new(document_data), Some(range)); + let json = parser.to_json().unwrap(); + let part_1 = include_str!("../assets/json/range_1.json"); + let part_1_json = serde_json::from_str::(part_1).unwrap(); + assert_eq!(part_1_json, json); +} + +// range_2 is a range from the 4th block's first child to the 18th block's first child +#[tokio::test] +async fn document_data_to_json_with_range_2_test() { + let initial_json_str = include_str!("../assets/json/initial_document.json"); + let document_data: DocumentData = JsonToDocumentParser::json_str_to_document(initial_json_str) + .unwrap() + .into(); + + let children_map = &document_data.meta.children_map; + let page_block_id = &document_data.page_id; + let blocks = &document_data.blocks; + let page_block = blocks.get(page_block_id).unwrap(); + + let start_block_parent_id = children_map + .get(page_block.children.as_str()) + .unwrap() + .get(3) + .unwrap(); + let start_block_parent = blocks.get(start_block_parent_id).unwrap(); + let start_block_id = children_map + .get(start_block_parent.children.as_str()) + .unwrap() + .get(0) + .unwrap(); + + let start = Selection { + block_id: start_block_id.to_string(), + index: 6, + length: 27, + }; + + let end_block_parent_id = children_map + .get(page_block.children.as_str()) + .unwrap() + .get(17) + .unwrap(); + let end_block_parent = blocks.get(end_block_parent_id).unwrap(); + let end_block_children = children_map + .get(end_block_parent.children.as_str()) + .unwrap(); + let end_block_id = end_block_children.get(0).unwrap(); + let end = Selection { + block_id: end_block_id.to_string(), + index: 0, + length: 11, + }; + + let range = Range { start, end }; + let parser = DocumentDataParser::new(Arc::new(document_data), Some(range)); + let json = parser.to_json().unwrap(); + let part_2 = include_str!("../assets/json/range_2.json"); + let part_2_json = serde_json::from_str::(part_2).unwrap(); + assert_eq!(part_2_json, json); +} diff --git a/frontend/rust-lib/flowy-document2/tests/parser/html_text/mod.rs b/frontend/rust-lib/flowy-document2/tests/parser/html_text/mod.rs new file mode 100644 index 0000000000000..60914e9678d00 --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/parser/html_text/mod.rs @@ -0,0 +1,2 @@ +mod test; +mod utils; diff --git a/frontend/rust-lib/flowy-document2/tests/parser/html_text/test.rs b/frontend/rust-lib/flowy-document2/tests/parser/html_text/test.rs new file mode 100644 index 0000000000000..9935443a14c0a --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/parser/html_text/test.rs @@ -0,0 +1,37 @@ +use crate::parser::html_text::utils::{assert_document_html_eq, assert_document_text_eq}; + +macro_rules! generate_test_cases { + ($($block_ty:ident),*) => { + [ + $( + ( + include_str!(concat!("../../assets/json/", stringify!($block_ty), ".json")), + include_str!(concat!("../../assets/html/", stringify!($block_ty), ".html")), + include_str!(concat!("../../assets/text/", stringify!($block_ty), ".txt")), + ) + ),* + ] + }; +} + +#[tokio::test] +async fn block_tests() { + let test_cases = generate_test_cases!( + heading, + callout, + paragraph, + divider, + image, + math_equation, + code, + bulleted_list, + numbered_list, + todo_list, + toggle_list, + quote + ); + for (json_data, expect_html, expect_text) in test_cases.iter() { + assert_document_html_eq(json_data, expect_html); + assert_document_text_eq(json_data, expect_text); + } +} diff --git a/frontend/rust-lib/flowy-document2/tests/parser/html_text/utils.rs b/frontend/rust-lib/flowy-document2/tests/parser/html_text/utils.rs new file mode 100644 index 0000000000000..5484da9ede86f --- /dev/null +++ b/frontend/rust-lib/flowy-document2/tests/parser/html_text/utils.rs @@ -0,0 +1,21 @@ +use flowy_document2::parser::document_data_parser::DocumentDataParser; +use flowy_document2::parser::json::parser::JsonToDocumentParser; +use std::sync::Arc; + +pub fn assert_document_html_eq(source: &str, expect: &str) { + let document_data = JsonToDocumentParser::json_str_to_document(source) + .unwrap() + .into(); + let parser = DocumentDataParser::new(Arc::new(document_data), None); + let html = parser.to_html(); + assert_eq!(expect, html); +} + +pub fn assert_document_text_eq(source: &str, expect: &str) { + let document_data = JsonToDocumentParser::json_str_to_document(source) + .unwrap() + .into(); + let parser = DocumentDataParser::new(Arc::new(document_data), None); + let text = parser.to_text(); + assert_eq!(expect, text); +} diff --git a/frontend/rust-lib/flowy-document2/tests/parser/mod.rs b/frontend/rust-lib/flowy-document2/tests/parser/mod.rs index cff0e9089e511..18ec9c9976816 100644 --- a/frontend/rust-lib/flowy-document2/tests/parser/mod.rs +++ b/frontend/rust-lib/flowy-document2/tests/parser/mod.rs @@ -1 +1,3 @@ +mod document_data_parser_test; +mod html_text; mod json;