mirror of
https://github.com/AppFlowy-IO/AppFlowy.git
synced 2024-08-30 18:12:39 +00:00
feat: support converting documents to JSON, HTML, or TEXT. (#3811)
* feat: support converting documents to JSON, HTML, or TEXT * fix: modify the comment * fix: modify the comment
This commit is contained in:
@ -36,7 +36,7 @@ export const turnToBlockThunk = createAsyncThunk(
|
|||||||
let caretId,
|
let caretId,
|
||||||
caretIndex = caret?.index || 0;
|
caretIndex = caret?.index || 0;
|
||||||
const deltaOperator = new BlockDeltaOperator(documentState, controller);
|
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
|
// insert new block after current block
|
||||||
const insertActions = [];
|
const insertActions = [];
|
||||||
|
|
||||||
@ -44,14 +44,14 @@ export const turnToBlockThunk = createAsyncThunk(
|
|||||||
delta = new Delta([{ insert: node.data.formula }]);
|
delta = new Delta([{ insert: node.data.formula }]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (delta && type === BlockType.EquationBlock) {
|
if (type === BlockType.EquationBlock) {
|
||||||
data.formula = deltaOperator.getDeltaText(delta);
|
data.formula = deltaOperator.getDeltaText(delta);
|
||||||
const block = newBlock<any>(type, parent.id, data);
|
const block = newBlock<any>(type, parent.id, data);
|
||||||
|
|
||||||
insertActions.push(controller.getInsertAction(block, node.id));
|
insertActions.push(controller.getInsertAction(block, node.id));
|
||||||
caretId = block.id;
|
caretId = block.id;
|
||||||
caretIndex = 0;
|
caretIndex = 0;
|
||||||
} else if (delta && type === BlockType.DividerBlock) {
|
} else if (type === BlockType.DividerBlock) {
|
||||||
const block = newBlock<any>(type, parent.id, data);
|
const block = newBlock<any>(type, parent.id, data);
|
||||||
|
|
||||||
insertActions.push(controller.getInsertAction(block, node.id));
|
insertActions.push(controller.getInsertAction(block, node.id));
|
||||||
@ -68,7 +68,7 @@ export const turnToBlockThunk = createAsyncThunk(
|
|||||||
caretId = nodeId;
|
caretId = nodeId;
|
||||||
caretIndex = 0;
|
caretIndex = 0;
|
||||||
insertActions.push(...actions);
|
insertActions.push(...actions);
|
||||||
} else if (delta) {
|
} else {
|
||||||
caretId = generateId();
|
caretId = generateId();
|
||||||
|
|
||||||
const actions = deltaOperator.getNewTextLineActions({
|
const actions = deltaOperator.getNewTextLineActions({
|
||||||
|
@ -4,6 +4,9 @@ use serde_json::Value;
|
|||||||
|
|
||||||
use flowy_document2::entities::*;
|
use flowy_document2::entities::*;
|
||||||
use flowy_document2::event_map::DocumentEvent;
|
use flowy_document2::event_map::DocumentEvent;
|
||||||
|
use flowy_document2::parser::parser_entities::{
|
||||||
|
ConvertDocumentPayloadPB, ConvertDocumentResponsePB,
|
||||||
|
};
|
||||||
use flowy_folder2::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB};
|
use flowy_folder2::entities::{CreateViewPayloadPB, ViewLayoutPB, ViewPB};
|
||||||
use flowy_folder2::event_map::FolderEvent;
|
use flowy_folder2::event_map::FolderEvent;
|
||||||
|
|
||||||
@ -108,6 +111,19 @@ impl DocumentEventTest {
|
|||||||
.await;
|
.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::<ConvertDocumentResponsePB>()
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn create_text(&self, payload: TextDeltaPayloadPB) {
|
pub async fn create_text(&self, payload: TextDeltaPayloadPB) {
|
||||||
let core = &self.inner;
|
let core = &self.inner;
|
||||||
EventBuilder::new(core.clone())
|
EventBuilder::new(core.clone())
|
||||||
|
@ -2,6 +2,7 @@ use collab_document::blocks::json_str_to_hashmap;
|
|||||||
use event_integration::document::document_event::DocumentEventTest;
|
use event_integration::document::document_event::DocumentEventTest;
|
||||||
use event_integration::document::utils::*;
|
use event_integration::document::utils::*;
|
||||||
use flowy_document2::entities::*;
|
use flowy_document2::entities::*;
|
||||||
|
use flowy_document2::parser::parser_entities::{ConvertDocumentPayloadPB, ExportTypePB};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
@ -120,3 +121,35 @@ async fn apply_text_delta_test() {
|
|||||||
json!([{ "insert": "Hello! World" }]).to_string()
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -1,3 +1,3 @@
|
|||||||
# Check out the FlowyConfig (located in flowy_toml.rs) for more details.
|
# 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"]
|
event_files = ["src/event_map.rs"]
|
@ -15,6 +15,11 @@ use flowy_error::{FlowyError, FlowyResult};
|
|||||||
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
|
use lib_dispatch::prelude::{data_result_ok, AFPluginData, AFPluginState, DataResult};
|
||||||
|
|
||||||
use crate::entities::*;
|
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};
|
use crate::{manager::DocumentManager, parser::json::parser::JsonToDocumentParser};
|
||||||
|
|
||||||
fn upgrade_document(
|
fn upgrade_document(
|
||||||
@ -303,3 +308,45 @@ impl From<(&Vec<BlockEvent>, 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<Weak<DocumentManager>>
|
||||||
|
|
||||||
|
* @return DataResult<[ConvertDocumentResponsePB], FlowyError>
|
||||||
|
*/
|
||||||
|
pub async fn convert_document(
|
||||||
|
data: AFPluginData<ConvertDocumentPayloadPB>,
|
||||||
|
manager: AFPluginState<Weak<DocumentManager>>,
|
||||||
|
) -> DataResult<ConvertDocumentResponsePB, FlowyError> {
|
||||||
|
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)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
@ -5,6 +5,7 @@ use strum_macros::Display;
|
|||||||
use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
|
use flowy_derive::{Flowy_Event, ProtoBuf_Enum};
|
||||||
use lib_dispatch::prelude::AFPlugin;
|
use lib_dispatch::prelude::AFPlugin;
|
||||||
|
|
||||||
|
use crate::event_handler::convert_document;
|
||||||
use crate::event_handler::get_snapshot_handler;
|
use crate::event_handler::get_snapshot_handler;
|
||||||
use crate::{event_handler::*, manager::DocumentManager};
|
use crate::{event_handler::*, manager::DocumentManager};
|
||||||
|
|
||||||
@ -27,6 +28,7 @@ pub fn init(document_manager: Weak<DocumentManager>) -> AFPlugin {
|
|||||||
.event(DocumentEvent::GetDocumentSnapshots, get_snapshot_handler)
|
.event(DocumentEvent::GetDocumentSnapshots, get_snapshot_handler)
|
||||||
.event(DocumentEvent::CreateText, create_text_handler)
|
.event(DocumentEvent::CreateText, create_text_handler)
|
||||||
.event(DocumentEvent::ApplyTextDeltaEvent, apply_text_delta_handler)
|
.event(DocumentEvent::ApplyTextDeltaEvent, apply_text_delta_handler)
|
||||||
|
.event(DocumentEvent::ConvertDocument, convert_document)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)]
|
#[derive(Debug, Clone, PartialEq, Eq, Hash, Display, ProtoBuf_Enum, Flowy_Event)]
|
||||||
@ -76,4 +78,49 @@ pub enum DocumentEvent {
|
|||||||
|
|
||||||
#[event(input = "TextDeltaPayloadPB")]
|
#[event(input = "TextDeltaPayloadPB")]
|
||||||
ApplyTextDeltaEvent = 11,
|
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("<p>Hello</p><p> World!</p>".to_string()));
|
||||||
|
/// ```
|
||||||
|
/// #
|
||||||
|
#[event(
|
||||||
|
input = "ConvertDocumentPayloadPB",
|
||||||
|
output = "ConvertDocumentResponsePB"
|
||||||
|
)]
|
||||||
|
ConvertDocument = 12,
|
||||||
}
|
}
|
||||||
|
37
frontend/rust-lib/flowy-document2/src/parser/constant.rs
Normal file
37
frontend/rust-lib/flowy-document2/src/parser/constant.rs
Normal file
@ -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";
|
@ -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<DocumentData>,
|
||||||
|
/// The range of the document data to parse. If the range is None, the entire document data will be parsed.
|
||||||
|
pub range: Option<Range>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DocumentDataParser {
|
||||||
|
pub fn new(document_data: Arc<DocumentData>, range: Option<Range>) -> Self {
|
||||||
|
Self {
|
||||||
|
document_data,
|
||||||
|
range,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts the JSON to an HTML representation.
|
||||||
|
pub fn to_html_with_json(&self, json: &Option<NestedBlock>) -> String {
|
||||||
|
let mut html = String::new();
|
||||||
|
html.push_str("<meta charset=\"UTF-8\">");
|
||||||
|
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<NestedBlock>) -> 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<NestedBlock> {
|
||||||
|
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<String>) -> Vec<String> {
|
||||||
|
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<String, Arc<Block>>, // in-range blocks
|
||||||
|
/// relation_map: HashMap<String, Arc<Vec<String>>>, // in-range blocks' children
|
||||||
|
/// delta_map: HashMap<String, String>, // 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1 +1,5 @@
|
|||||||
|
pub mod constant;
|
||||||
|
pub mod document_data_parser;
|
||||||
pub mod json;
|
pub mod json;
|
||||||
|
pub mod parser_entities;
|
||||||
|
pub mod utils;
|
||||||
|
481
frontend/rust-lib/flowy-document2/src/parser/parser_entities.rs
Normal file
481
frontend/rust-lib/flowy-document2/src/parser/parser_entities.rs
Normal file
@ -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<RangePB> - 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<RangePB>,
|
||||||
|
|
||||||
|
#[pb(index = 3)]
|
||||||
|
pub export_types: ExportTypePB,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Default, ProtoBuf, Debug)]
|
||||||
|
pub struct ConvertDocumentResponsePB {
|
||||||
|
#[pb(index = 1, one_of)]
|
||||||
|
pub json: Option<String>,
|
||||||
|
#[pb(index = 2, one_of)]
|
||||||
|
pub html: Option<String>,
|
||||||
|
#[pb(index = 3, one_of)]
|
||||||
|
pub text: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<Range>,
|
||||||
|
pub export_types: ExportType,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ExportType {
|
||||||
|
pub fn any_enabled(&self) -> bool {
|
||||||
|
self.json || self.html || self.text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<SelectionPB> for Selection {
|
||||||
|
fn from(data: SelectionPB) -> Self {
|
||||||
|
Selection {
|
||||||
|
block_id: data.block_id,
|
||||||
|
index: data.index,
|
||||||
|
length: data.length,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<RangePB> for Range {
|
||||||
|
fn from(data: RangePB) -> Self {
|
||||||
|
Range {
|
||||||
|
start: data.start.into(),
|
||||||
|
end: data.end.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ExportTypePB> for ExportType {
|
||||||
|
fn from(data: ExportTypePB) -> Self {
|
||||||
|
ExportType {
|
||||||
|
json: data.json,
|
||||||
|
html: data.html,
|
||||||
|
text: data.text,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl TryInto<ConvertDocumentParams> for ConvertDocumentPayloadPB {
|
||||||
|
type Error = ErrorCode;
|
||||||
|
fn try_into(self) -> Result<ConvertDocumentParams, Self::Error> {
|
||||||
|
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<HashMap<String, Value>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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!("<a href={}>", href));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Serialize the code attributes.
|
||||||
|
if let Some(code) = attrs.get(CODE) {
|
||||||
|
if code.as_bool().unwrap_or(false) {
|
||||||
|
html.push_str("<code>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 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!("<span style=\"{}\">", style));
|
||||||
|
}
|
||||||
|
// Serialize the insert field.
|
||||||
|
html.push_str(&self.insert);
|
||||||
|
|
||||||
|
// Close the style tag.
|
||||||
|
if !style.is_empty() {
|
||||||
|
html.push_str("</span>");
|
||||||
|
}
|
||||||
|
// Close the tags: <a>, <code>.
|
||||||
|
if let Some(attrs) = &self.attributes {
|
||||||
|
if attrs.contains_key(HREF) {
|
||||||
|
html.push_str("</a>");
|
||||||
|
}
|
||||||
|
if attrs.contains_key(CODE) {
|
||||||
|
html.push_str("</code>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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<String, Value>,
|
||||||
|
#[serde(default)]
|
||||||
|
pub children: Vec<NestedBlock>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String>,
|
||||||
|
pub next_block_ty: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NestedBlock {
|
||||||
|
pub fn new(
|
||||||
|
id: String,
|
||||||
|
ty: String,
|
||||||
|
data: HashMap<String, Value>,
|
||||||
|
children: Vec<NestedBlock>,
|
||||||
|
) -> 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!("<h6>{}</h6>", text_html));
|
||||||
|
} else {
|
||||||
|
html.push_str(&format!("<h{}>{}</h{}>", level, text_html, level));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PARAGRAPH => {
|
||||||
|
html.push_str(&format!("<p>{}</p>", text_html));
|
||||||
|
html.push_str(&convert_nested_block_children_to_html(Arc::new(
|
||||||
|
self.to_owned(),
|
||||||
|
)));
|
||||||
|
},
|
||||||
|
CALLOUT => {
|
||||||
|
html.push_str(&format!(
|
||||||
|
"<p>{}{}</p>",
|
||||||
|
self
|
||||||
|
.data
|
||||||
|
.get(ICON)
|
||||||
|
.unwrap_or(&Value::Null)
|
||||||
|
.to_string()
|
||||||
|
.trim_matches('\"'),
|
||||||
|
text_html
|
||||||
|
));
|
||||||
|
},
|
||||||
|
IMAGE => {
|
||||||
|
html.push_str(&format!(
|
||||||
|
"<img src={} alt={} />",
|
||||||
|
self.data.get(URL).unwrap(),
|
||||||
|
"AppFlowy-Image"
|
||||||
|
));
|
||||||
|
},
|
||||||
|
DIVIDER => {
|
||||||
|
html.push_str("<hr />");
|
||||||
|
},
|
||||||
|
MATH_EQUATION => {
|
||||||
|
let formula = self.data.get(FORMULA).unwrap_or(&Value::Null);
|
||||||
|
html.push_str(&format!(
|
||||||
|
"<p>{}</p>",
|
||||||
|
formula.to_string().trim_matches('\"')
|
||||||
|
));
|
||||||
|
},
|
||||||
|
CODE => {
|
||||||
|
let language = self.data.get(LANGUAGE).unwrap_or(&Value::Null);
|
||||||
|
html.push_str(&format!(
|
||||||
|
"<pre><code class=\"language-{}\">{}</code></pre>",
|
||||||
|
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!("<li>[{}] {}</li>", checked_str, text_html));
|
||||||
|
} else {
|
||||||
|
html.push_str(&format!("<li>{}</li>", 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("<blockquote>");
|
||||||
|
}
|
||||||
|
html.push_str(&format!("<p>{}</p>", 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("</blockquote>");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
PAGE => {
|
||||||
|
if !text_html.is_empty() {
|
||||||
|
html.push_str(&format!("<p>{}</p>", text_html));
|
||||||
|
}
|
||||||
|
html.push_str(&convert_nested_block_children_to_html(Arc::new(
|
||||||
|
self.to_owned(),
|
||||||
|
)));
|
||||||
|
},
|
||||||
|
_ => {
|
||||||
|
html.push_str(&format!("<p>{}</p>", 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
|
||||||
|
}
|
||||||
|
}
|
167
frontend/rust-lib/flowy-document2/src/parser/utils.rs
Normal file
167
frontend/rust-lib/flowy-document2/src/parser/utils.rs
Normal file
@ -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<String, Arc<Block>>,
|
||||||
|
pub(crate) relation_map: HashMap<String, Arc<Vec<String>>>,
|
||||||
|
pub(crate) delta_map: HashMap<String, Vec<InsertDelta>>,
|
||||||
|
}
|
||||||
|
pub fn block_to_nested_json(
|
||||||
|
block_id: &str,
|
||||||
|
convert_params: &ConvertBlockToJsonParams,
|
||||||
|
) -> Option<NestedBlock> {
|
||||||
|
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<String> {
|
||||||
|
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<Vec<InsertDelta>> {
|
||||||
|
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::<Vec<InsertDelta>>(delta_str).ok()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_delta_for_selection(
|
||||||
|
selection: &Selection,
|
||||||
|
data: &DocumentData,
|
||||||
|
) -> Option<Vec<InsertDelta>> {
|
||||||
|
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<InsertDelta>, start: usize, end: usize) -> Vec<InsertDelta> {
|
||||||
|
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<InsertDelta>) -> 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<InsertDelta>) -> 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<NestedBlock>) -> 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<Vec<InsertDelta>> {
|
||||||
|
serde_json::from_value::<Vec<InsertDelta>>(delta_value.to_owned()).ok()
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
<meta charset="UTF-8"><ul><li>Highlight</li><p>You can also</p><ul><li>nest</li></ul></ul>
|
@ -0,0 +1,6 @@
|
|||||||
|
<meta charset="UTF-8"><p>🥰
|
||||||
|
Like AppFlowy? Follow us:
|
||||||
|
<a href="https://github.com/AppFlowy-IO/AppFlowy">GitHub</a>
|
||||||
|
<a href="https://twitter.com/appflowy">Twitter</a>: @appflowy
|
||||||
|
<a href="https://blog-appflowy.ghost.io/">Newsletter</a>
|
||||||
|
</p>
|
@ -0,0 +1,5 @@
|
|||||||
|
<meta charset="UTF-8"><pre><code class="language-rust">// This is the main function.
|
||||||
|
fn main() {
|
||||||
|
// Print text to the console.
|
||||||
|
println!("Hello World!");
|
||||||
|
}</code></pre>
|
@ -0,0 +1 @@
|
|||||||
|
<meta charset="UTF-8"><hr />
|
@ -0,0 +1 @@
|
|||||||
|
<meta charset="UTF-8"><h1>Heading1</h1><h2>Heading2</h2><h3>Heading3</h3>
|
@ -0,0 +1 @@
|
|||||||
|
<meta charset="UTF-8"><img src="https://www.google.com/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png" alt=AppFlowy-Image />
|
@ -0,0 +1 @@
|
|||||||
|
<meta charset="UTF-8"><p>E = MC^2</p>
|
@ -0,0 +1 @@
|
|||||||
|
<meta charset="UTF-8"><ol><li>Highlight</li><p>You can also</p><ol><li>nest</li></ol></ol>
|
@ -0,0 +1,6 @@
|
|||||||
|
<meta charset="UTF-8"><p>
|
||||||
|
Like AppFlowy? Follow us:
|
||||||
|
<a href="https://github.com/AppFlowy-IO/AppFlowy">GitHub</a>
|
||||||
|
<a href="https://twitter.com/appflowy">Twitter</a>: @appflowy
|
||||||
|
<a href="https://blog-appflowy.ghost.io/">Newsletter</a>
|
||||||
|
</p><p>Click <code>?</code> at the bottom right for help and support.</p><p><span style="background-color: #4dffeb3b;">Highlight </span>any text, and use the editing menu to <span style="font-style: italic;">style</span> <span style="font-weight: bold;">your</span> <span style="text-decoration: underline;">writing</span> <code>however</code><span style="color: #4dffeb3b;"> you </span><span style="text-decoration: line-through;">like.</span><span style="font-family: fantasy;">1+1=2</span></p>
|
@ -0,0 +1 @@
|
|||||||
|
<meta charset="UTF-8"><blockquote><p>This is a quote</p><p>This is a paragraph</p></blockquote>
|
@ -0,0 +1 @@
|
|||||||
|
<meta charset="UTF-8"><ul><li>[x] Highlight</li><p>You can also</p><ul><li>[ ] nest</li></ul></ul>
|
@ -0,0 +1 @@
|
|||||||
|
<meta charset="UTF-8"><ul><li>Click <code>?</code> at the bottom right for help and support.</li><p>This is a paragraph</p><ul><li>This is a toggle list</li></ul></ul>
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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": "🥰"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"type": "page",
|
||||||
|
"data": {},
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"type": "divider",
|
||||||
|
"data": {}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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": [] } }
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"type": "page",
|
||||||
|
"children": [{
|
||||||
|
"type": "math_equation",
|
||||||
|
"data": {
|
||||||
|
"formula": "E = MC^2"
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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" }
|
||||||
|
] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
100
frontend/rust-lib/flowy-document2/tests/assets/json/range_1.json
Normal file
100
frontend/rust-lib/flowy-document2/tests/assets/json/range_1.json
Normal file
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
178
frontend/rust-lib/flowy-document2/tests/assets/json/range_2.json
Normal file
178
frontend/rust-lib/flowy-document2/tests/assets/json/range_2.json
Normal file
@ -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": []
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -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" }] }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
Highlight
|
||||||
|
You can also
|
||||||
|
nest
|
@ -0,0 +1,6 @@
|
|||||||
|
🥰
|
||||||
|
Like AppFlowy? Follow us:
|
||||||
|
GitHub
|
||||||
|
Twitter: @appflowy
|
||||||
|
Newsletter
|
||||||
|
|
@ -0,0 +1,5 @@
|
|||||||
|
// This is the main function.
|
||||||
|
fn main() {
|
||||||
|
// Print text to the console.
|
||||||
|
println!("Hello World!");
|
||||||
|
}
|
@ -0,0 +1 @@
|
|||||||
|
|
@ -0,0 +1,3 @@
|
|||||||
|
Heading1
|
||||||
|
Heading2
|
||||||
|
Heading3
|
@ -0,0 +1 @@
|
|||||||
|
|
@ -0,0 +1 @@
|
|||||||
|
E = MC^2
|
@ -0,0 +1,3 @@
|
|||||||
|
Highlight
|
||||||
|
You can also
|
||||||
|
nest
|
@ -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
|
@ -0,0 +1,2 @@
|
|||||||
|
This is a quote
|
||||||
|
This is a paragraph
|
@ -0,0 +1,3 @@
|
|||||||
|
Highlight
|
||||||
|
You can also
|
||||||
|
nest
|
@ -0,0 +1,3 @@
|
|||||||
|
Click ? at the bottom right for help and support.
|
||||||
|
This is a paragraph
|
||||||
|
This is a toggle list
|
@ -2,4 +2,4 @@ mod document_insert_test;
|
|||||||
mod document_redo_undo_test;
|
mod document_redo_undo_test;
|
||||||
mod document_test;
|
mod document_test;
|
||||||
mod event_handler_test;
|
mod event_handler_test;
|
||||||
mod util;
|
pub mod util;
|
||||||
|
@ -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::<NestedBlock>(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::<NestedBlock>(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::<NestedBlock>(part_2).unwrap();
|
||||||
|
assert_eq!(part_2_json, json);
|
||||||
|
}
|
@ -0,0 +1,2 @@
|
|||||||
|
mod test;
|
||||||
|
mod utils;
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
@ -1 +1,3 @@
|
|||||||
|
mod document_data_parser_test;
|
||||||
|
mod html_text;
|
||||||
mod json;
|
mod json;
|
||||||
|
Reference in New Issue
Block a user